diff options
author | George Hunt <ghunt@ghunt-desktop.(none)> | 2010-05-14 14:54:09 (GMT) |
---|---|---|
committer | George Hunt <ghunt@ghunt-desktop.(none)> | 2010-05-14 14:54:09 (GMT) |
commit | 580526e849afb84244b7a0373d8fdb8f5ffe0103 (patch) | |
tree | e118e3adb44be31b21d2347ad5658b2db18a5532 |
initial upload, working: sqlite, datastore access, thumbnails, mouse clicks. Not implemented: drag/drop, add albums, create camera rolls, help, etc
-rw-r--r-- | MANIFEST | 32 | ||||
-rw-r--r-- | activity/XoPhoto.svg | 111 | ||||
-rw-r--r-- | activity/activity.info | 6 | ||||
-rw-r--r-- | activity/activity.info.gh | 8 | ||||
-rw-r--r-- | activity/activity.save | 0 | ||||
-rw-r--r-- | activity/scommander.svg | 88 | ||||
-rw-r--r-- | dbphoto.py | 277 | ||||
-rw-r--r-- | display.py | 486 | ||||
-rw-r--r-- | dist/XoPhoto-1.xo | bin | 0 -> 694591 bytes | |||
-rw-r--r-- | photo_toolbar.py | 216 | ||||
-rwxr-xr-x | setup.py | 21 | ||||
-rw-r--r-- | sources.py | 181 | ||||
-rw-r--r-- | sugargame/__init__.py | 1 | ||||
-rw-r--r-- | sugargame/canvas.py | 56 | ||||
-rw-r--r-- | sugargame/event.py | 241 | ||||
-rw-r--r-- | xophoto.sqlite | bin | 0 -> 958464 bytes | |||
-rw-r--r-- | xophotoactivity.py | 289 |
17 files changed, 2013 insertions, 0 deletions
diff --git a/MANIFEST b/MANIFEST new file mode 100644 index 0000000..d4efdb4 --- /dev/null +++ b/MANIFEST @@ -0,0 +1,32 @@ +xophoto.sqlite +dbphoto.py +xophoto +image_datastore.py +dbaccess.py +TestGame.py +TestActivity.py +test_blob.py +dsget.py +sugarcommander.py +display.py +DemoiselleActivity.py +xotest.py +dbphoto.pyo +sources.py +sources.pyo +xophotoactivity.py +xophotoactivity.pyo +display.pyo +setup.py +po/SugarCommander.pot +activity/scommander.svg +activity/activity.info +activity/XoPhoto.svg +activity/activity.save +activity/activity.info.gh +sugargame/canvas.py +sugargame/canvas.pyo +sugargame/__init__.pyo +sugargame/event.pyo +sugargame/event.py +sugargame/__init__.py diff --git a/activity/XoPhoto.svg b/activity/XoPhoto.svg new file mode 100644 index 0000000..8a1ced3 --- /dev/null +++ b/activity/XoPhoto.svg @@ -0,0 +1,111 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> +<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [ + <!ENTITY stroke_color "#000000"> + <!ENTITY fill_color "#ffffff"> +]> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + 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" + width="55" + height="55" + id="svg4502" + sodipodi:version="0.32" + inkscape:version="0.46" + version="1.0" + sodipodi:docbase="/Users/eben/Desktop" + sodipodi:docname="circle.svg" + inkscape:output_extension="org.inkscape.output.svg.inkscape" + style="display:inline"> + <defs + id="defs4504"> + <inkscape:perspective + sodipodi:type="inkscape:persp3d" + inkscape:vp_x="0 : 27.5 : 1" + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="55 : 27.5 : 1" + inkscape:persp3d-origin="27.5 : 18.333333 : 1" + id="perspective2408" /> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + gridtolerance="10000" + guidetolerance="10" + objecttolerance="10" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="3.959798" + inkscape:cx="30.611899" + inkscape:cy="12.891623" + inkscape:document-units="px" + inkscape:current-layer="layer4" + width="55px" + height="55px" + inkscape:window-width="701" + inkscape:window-height="722" + inkscape:window-x="285" + inkscape:window-y="68" + showgrid="false"> + <inkscape:grid + id="GridFromPre046Settings" + type="xygrid" + originx="0px" + originy="0px" + spacingx="15px" + spacingy="15px" + color="#0000ff" + empcolor="#0000ff" + opacity="0.2" + empopacity="0.4" + empspacing="5" /> + </sodipodi:namedview> + <metadata + id="metadata4507"> + <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> + <g + inkscape:groupmode="layer" + id="layer4" + inkscape:label="icon" + style="display:inline"> + <path + sodipodi:type="arc" + style="opacity:1;fill:&fill_color;;fill-opacity:1;stroke:&stroke_color;;stroke-width:2.43559003;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="path2165" + sodipodi:cx="29.546961" + sodipodi:cy="27.725882" + sodipodi:rx="15.657364" + sodipodi:ry="15.657364" + d="M 45.204325,27.725882 A 15.657364,15.657364 0 1 1 13.889597,27.725882 A 15.657364,15.657364 0 1 1 45.204325,27.725882 z" + transform="matrix(1.4370235,0,0,1.4370235,-14.959677,-12.342744)" /> + <text + xml:space="preserve" + style="font-size:19.38258234px;font-style:normal;font-weight:bold;fill:&fill_color;;fill-opacity:1;stroke:&stroke_color;;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans Bold;font-stretch:normal;font-variant:normal;text-anchor:start;text-align:start;writing-mode:lr;line-height:125%" + x="12.776501" + y="30.940331" + id="text2432" + transform="matrix(0.9107558,8.8314569e-3,-9.9819452e-3,1.0978923,0,0)" + inkscape:transform-center-x="6.6723495" + inkscape:transform-center-y="11.705454" + sodipodi:linespacing="125%"><tspan + sodipodi:role="line" + id="tspan2434" + x="12.776501" + y="30.940331">XP</tspan></text> + </g> +</svg> diff --git a/activity/activity.info b/activity/activity.info new file mode 100644 index 0000000..1426aee --- /dev/null +++ b/activity/activity.info @@ -0,0 +1,6 @@ +[Activity] +name = XoPhoto +bundle_id = org.laptop.XoPhoto +activity_version = 1 +icon = XoPhoto +class = xophotoactivity.XoPhotoActivity diff --git a/activity/activity.info.gh b/activity/activity.info.gh new file mode 100644 index 0000000..815e9c8 --- /dev/null +++ b/activity/activity.info.gh @@ -0,0 +1,8 @@ +[Activity] +name = Sugar Commander +service_name = org.laptop.sugar.SugarCommander +icon = scommander +exec = sugar-activity sugarcommander.SugarCommander +show_launcher = yes +activity_version = 3 +license = GPLv2+ diff --git a/activity/activity.save b/activity/activity.save new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/activity/activity.save diff --git a/activity/scommander.svg b/activity/scommander.svg new file mode 100644 index 0000000..67fc3b8 --- /dev/null +++ b/activity/scommander.svg @@ -0,0 +1,88 @@ +<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [ + <!ENTITY stroke_color "#000000"> + <!ENTITY fill_color "#ffffff"> +]> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + 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" + enable-background="new 0 0 55 55" + height="55px" + id="Layer_1" + version="1.1" + viewBox="0 0 55 55" + width="55px" + x="0px" + xml:space="preserve" + y="0px" + sodipodi:version="0.32" + inkscape:version="0.46" + sodipodi:docname="scommander.svg" + inkscape:output_extension="org.inkscape.output.svg.inkscape"><metadata + id="metadata2457"><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="defs2455"><inkscape:perspective + sodipodi:type="inkscape:persp3d" + inkscape:vp_x="0 : 27.5 : 1" + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="55 : 27.5 : 1" + inkscape:persp3d-origin="27.5 : 18.333333 : 1" + id="perspective2459" /></defs><sodipodi:namedview + inkscape:window-height="816" + inkscape:window-width="1152" + inkscape:pageshadow="2" + inkscape:pageopacity="0.0" + guidetolerance="10.0" + gridtolerance="10.0" + objecttolerance="10.0" + borderopacity="1.0" + bordercolor="#666666" + pagecolor="&fill_color;" + id="base" + showgrid="false" + inkscape:zoom="11.313708" + inkscape:cx="27.5" + inkscape:cy="27.5" + inkscape:window-x="-4" + inkscape:window-y="-4" + inkscape:current-layer="Layer_1" /><g + display="block" + id="activity-journal" + transform="translate(0.1767767,-0.7071068)" + style="display:block"> + <path + d="M 45.866,44.669 C 45.866,47.18 44.338,49 41.534,49 L 12.077,49 L 12.077,6 L 41.535,6 C 43.685,6 45.867,8.154 45.867,10.33 L 45.866,44.669 L 45.866,44.669 z" + id="path2444" + style="fill:&fill_color;;stroke:&stroke_color;;stroke-width:3.5;stroke-linecap:round;stroke-linejoin:round" /> + + <line + x1="21.341" + x2="21.341" + y1="6.1209998" + y2="48.881001" + id="line2446" + style="fill:none;stroke:&stroke_color;;stroke-width:3.5;stroke-linecap:round;stroke-linejoin:round" /> + <path + d="M 7.384,14.464 C 7.384,14.464 9.468,15.159 11.554,15.159 C 13.64,15.159 15.727,14.464 15.727,14.464" + id="path2448" + style="fill:none;stroke:&stroke_color;;stroke-width:3.5;stroke-linecap:round;stroke-linejoin:round" /> + <path + d="M 7.384,28.021 C 7.384,28.021 9.296,28.716 11.729,28.716 C 14.162,28.716 15.728,28.021 15.728,28.021" + id="path2450" + style="fill:none;stroke:&stroke_color;;stroke-width:3.5;stroke-linecap:round;stroke-linejoin:round" /> + <path + d="M 7.384,41.232 C 7.384,41.232 9.12,41.927 11.902,41.927 C 14.683,41.927 15.727,41.232 15.727,41.232" + id="path2452" + style="fill:none;stroke:&stroke_color;;stroke-width:3.5;stroke-linecap:round;stroke-linejoin:round" /> +</g><path + style="fill:none;fill-rule:evenodd;stroke:&stroke_color;;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="M 26.958446,28.660272 C 31.819805,23.091806 31.819805,23.091806 31.819805,23.091806 C 36.504388,28.129942 36.504388,28.129942 36.504388,28.129942" + id="path2469" /><path + style="fill:none;fill-rule:evenodd;stroke:&stroke_color;;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="M 26.870058,32.726136 C 31.643028,26.715729 31.643028,26.715729 31.643028,26.715729 C 35.974058,32.195806 35.974058,32.195806 35.974058,32.195806" + id="path2471" /></svg> diff --git a/dbphoto.py b/dbphoto.py new file mode 100644 index 0000000..1fe110d --- /dev/null +++ b/dbphoto.py @@ -0,0 +1,277 @@ +#!/usr/bin/env python +# dbphoto.py +# The sqlite database access functions for the XoPhoto application +# +# +# Copyright (C) 2010 George Hunt +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# +from gettext import gettext as _ + +import os +from sqlite3 import dbapi2 as sqlite +from sqlite3 import * +import sqlite3 +import hashlib + +#pick up activity globals +from xophotoactivity import * + +#define globals related to sqlite +sqlite_file_path = None + +import logging +_logger = logging.getLogger('xophoto') +_logger.setLevel(logging.DEBUG) +console_handler = logging.StreamHandler() +console_formatter = logging.Formatter('%(name)s %(levelname)s %(funcName)s: %(lineno)d||| %(message)s') +console_handler.setFormatter(console_formatter) +_logger.addHandler(console_handler) + + +class DbAccess(): + con = None + cursor = None + def __init__(self,fn): + self.opendb(fn) + self.added = 0 + + def opendb(self,dbfilename): + try: + _logger.debug('opening database cwd:%s filename %s'%(os.getcwd(),dbfilename,)) + self.con = sqlite3.connect(dbfilename) + self.con.row_factory = sqlite3.Row + self.con.text_factory = str + #rows generated thusly will have columns that are addressable as dict of fieldnames + self.cur = self.con.cursor() + except IOError,e: + _logger.debug('open database failed. exception :%s '%(e,)) + return None + return self.cur + + def is_open(self): + if self.con: return True + return False + + def closedb(self): + if self.con:self.con.close() + + def get_mime_list(self): + mime_list =[] + self.cur.execute('select * from config where name ="mime_type"') + rows = self.cur.fetchall() + for m in rows: + mime_list.append(m[2]) + return mime_list + + def get_album_list(self): + sql = 'select max duplicate from picture group by album' + album_list,cur = self.dbdo(sql) + if len(album_list) == 0: + _logger.debug('failed to retrieve albums') + return None + return album_list + + def create_picture_record(self,object_id, fn): + """create a record in picture pointing to unique pictures in the journal. + Use md5 checksum to test for uniqueness + For non unique entries, add a copy number (fieldname:duplicate) greater than 0 + """ + _logger.debug('create_picture_record object_id:%s file: %s'%(object_id,fn,)) + #if object_id == '': return + + #we'll calculate the md5, check it against any pictures, and store it away + md5_hash = Md5Tools().md5sum(fn) + sql = "select * from picture where md5_sum = '%s'"%(md5_hash,) + self.cur.execute(sql) + rows_md5 = self.cur.fetchall() + if len(rows_md5) >0: + pass + #_logger.debug('duplicate picture, ojbect_id %s path: %s'%(object_id,fn,)) + sql = "select * from picture where jobject_id = '%s'"%(object_id,) + self.cur.execute(sql) + rows = self.cur.fetchall() + #_logger.debug('rowcount %s object_id %s'%(len(rows),object_id)) + #the object_id is supposed to be unique, so add only new object_id's + if len(rows) == 0: + info = os.stat(fn) + sql = """insert into picture \ + (in_ds, mount_point, orig_size, create_date,jobject_id, md5_sum, duplicate) \ + values (%s,'%s',%s,'%s','%s','%s',%s)""" % \ + (1, fn, info.st_size, info.st_ctime, object_id, md5_hash,len(rows_md5),) + _logger.debug('sql: %s'%sql) + self.con.execute(sql) + + def clear_in_ds(self): + self.con.execute('update picture set in_ds = 0') + + def delete_not_in_ds(self): + self.con.execute('delete from picture where in_ds = 0') + + def check_in_ds(self,fullpath,size): + sql = "select * from picture where mount_point = '%s' and orig_size = %s"%(fullpath,size,) + self.cur.execute(sql) + rows = self.cur.fetchall() + if len(rows)>0: return True + return False + + def table_exists(self,table): + try: + sql = 'select * from %s'%table + self.con.execute(sql) + return True + except: + return False + + def commit(self): + if self.con:self.con.commit() + + def set_connection(self,connection,cursor): + self.con = connection + self.cur = cursor + + def get_connection(self): + """ return connection """ + return self.con + + def numberofrows(self,table): + sql = "SELECT count(*) from %s"%table + rows,cur = self.dbdo(sql) + if rows: + return rows[0][0] + return 0 + + def fieldlist(self,table): + list=[] #accumulator for model + cur = self.con.cursor() + cur.execute('select * from %s'%table) + if cur: + for field in cur.description: + list.append(field[0]) + return list + + def row_index(self,field,table): + field_list = self.fieldlist(table) + return field_list.index(field) + + def tablelist(self): + list=[] #accumulator for + sql = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name" + rows,cur = self.dbdo(sql) + if rows: + for row in rows: + list.append(row[0]) + return list + + def dbdo(self,sql): + """ execute a sql statement or definition, return rows and cursor """ + try: + cur = self.con.cursor() + cur.execute(sql) + return cur.fetchall(), cur + #self.con.commit() + except sqlite.Error, e: + print 'An sqlite error:',e.args[0] + print sql+'\n' + return None,None + + def dbtry(self,sql): + """ execute a sql statement return true if no error""" + try: + self.cur.execute(sql) + return True,None + except sqlite.Error, e: + print sql+'\n' + return False,e +""" +class osfsys(): + + def havewriteaccess(self,writefile): + try: + fh=open(writefile,'w+') + fh.close() + return True + except: + return False + + def havereadaccess(self,readfile): + try: + fh=open(readfile,'r') + fh.close() + return True + except: + return False +""" +class Md5Tools(): + def md5sum_buffer(self, buffer, hash = None): + if hash == None: + hash = hashlib.md5() + hash.update(buffer) + return hash.hexdigest() + + def md5sum(self, filename, hash = None): + h = self._md5sum(filename,hash) + return h.hexdigest() + + def _md5sum(self, filename, hash = None): + if hash == None: + hash = hashlib.md5() + try: + fd = None + fd = open(filename, 'rb') + while True: + block = fd.read(128) + if not block: break + hash.update(block) + finally: + if fd != None: + fd.close() + return hash + + def md5sum_tree(self,root): + if not os.path.isdir(root): + return None + h = hashlib.md5() + for dirpath, dirnames, filenames in os.walk(root): + for filename in filenames: + abs_path = os.path.join(dirpath, filename) + h = self._md5sum(abs_path,h) + #print abs_path + return h.hexdigest() + + def set_permissions(self,root, perms='664'): + if not os.path.isdir(root): + return None + for dirpath, dirnames, filenames in os.walk(root): + for filename in filenames: + abs_path = os.path.join(dirpath, filename) + old_perms = os.stat(abs_path).st_mode + if os.path.isdir(abs_path): + new_perms = int(perms,8) | int('771',8) + else: + new_perms = old_perms | int(perms,8) + os.chmod(abs_path,new_perms) + +if __name__ == '__main__': + db = DbAccess('xophoto.sqlite') + rows,cur = db.dbdo('select * from picture') + for row in rows: + print row['jobject_id'] + print('index of jobject_id: %s'%db.row_index('duplicate','picture')) + print('number of records %s'%db.numberofrows('picture')) + print('fields %r'%db.fieldlist('picture')) + print ('tables %r'%db.tablelist()) +
\ No newline at end of file diff --git a/display.py b/display.py new file mode 100644 index 0000000..3bac79d --- /dev/null +++ b/display.py @@ -0,0 +1,486 @@ +#!/usr/bin/env python +# display.py +# +# Copyright (C) 2010 George Hunt +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# +from gettext import gettext as _ + +import pygame +from pygame.locals import * + +from sugar.datastore import datastore +import sys, os +import gtk +import shutil +import sqlite3 +from sqlite3 import dbapi2 as sqlite +import math +import hashlib +import time +from threading import Timer + +#application imports +from dbphoto import * +from sources import * + + +#pick up activity globals +from xophotoactivity import * + +#Display Module globals +background_color = (255,255,255) +selected_color = (0,0,255) +mouse_timer = time.time() +in_click_delay = False +in_drag = False + +import logging +_logger = logging.getLogger('xophoto.display') + +_logger.setLevel(logging.DEBUG) +console_handler = logging.StreamHandler() +console_formatter = logging.Formatter('%(name)s %(levelname)s %(funcName)s: %(lineno)d||| %(message)s') +console_handler.setFormatter(console_formatter) +_logger.addHandler(console_handler) + +class PhotoException(Exception): + def __init__(self,value): + self.value = value + def __str__(): + return repr(self.value) + +class DisplayOne(): + def __init__(self,rows,db,index=0): + self.rows = rows + self.db = db + self.row_index = index + self.border = 5 + self.x = 200 + self.y = 200 + self.size_x = 800 + self.size_y = 800 + self.surf = None + self.background = (200,200,200) + self.scaled = None + + def paint(self): + """ + Put an image on pygame screen. + Inputs: 1. cursor pointing to picture records of xophoto.sqlite + 2. Index into cursor + """ + if not self.scaled: + i = self.db.row_index('jobject_id','picture') + id = self.rows[self.row_index][i] + self.surface = pygame.Surface((self.size_x,self.size_y,)) + self.surface.fill(background_color) + self.scaled = self.scale_image(id,self.size_x,self.size_y) + if not self.scaled: return + if self.aspect >= 1.0: + self.subsurface_x = self.border + self.subsurface_y = (self.size_y - self.y_thumb) // 2 + else: + self.subsurface_y = self.border + self.subsurface_x = (self.size_x - self.x_thumb) // 2 + self.thumbnail = self.surface.subsurface([self.subsurface_x,self.subsurface_y,self.x_thumb,self.y_thumb]) + self.thumbnail.blit(self.scaled,[0,0]) + screen.blit(self.surface,[self.x,self.y]) + + def scale_image(self,id, x_size, y_size): + """ + First check to see if this thumbnail is already in the database, if so, return it + If not, generate the thumbnail, write it to the database, and return it + """ + max_dim = max(x_size,y_size) - 2*self.border + sql = 'select * from transforms where jobject_id = "%s"'%id + rows, cur = self.db.dbdo(sql) + for row in rows: + w = row['scaled_x'] + h = row['scaled_y'] + transform_max = max(w,h) + _logger.debug('transform rec max: %s request max: %s'%(transform_max,max_dim,)) + if max_dim == transform_max: + self.x_thumb = w + self.y_thumb = h + self.aspect = float(w)/h + blob =row['thumb'] + surf = pygame.image.frombuffer(blob,(w,h),'RGB') + _logger.debug('retrieved thumbnail from database') + return surf + try: + ds_obj = datastore.get(id) + except Exception,e: + print('get filename from id error: %s'%e) + return None + if ds_obj: + fn = ds_obj.get_file_path() + try: + self.surf = pygame.image.load(fn) + except Exception,e: + print('scale_image failed to load %s'%fn) + return None + finally: + ds_obj.destroy() + self.surf.convert + w,h = self.surf.get_size() + self.aspect = float(w)/h + if self.aspect > 1.0: + self.x_thumb = int(x_size - 2 * self.border) + self.y_thumb = int((y_size - 2 * self.border) / self.aspect) + else: + self.x_thumb = int((x_size - 2 * self.border) * self.aspect) + self.y_thumb = int((y_size - 2 * self.border)) + thumb_size = (self.x_thumb,self.y_thumb) + ret = pygame.transform.scale(self.surf,thumb_size) + #write the transform to the database for speedup next time + thumbstr = pygame.image.tostring(ret,'RGB') + conn = self.db.get_connection() + cursor = conn.cursor() + thumb_binary = sqlite3.Binary(thumbstr) + try: + cursor.execute("insert into transforms (jobject_id,original_x,original_y,scaled_x,scaled_y,thumb) values (?,?,?,?,?,?)",\ + (id,w,h,self.x_thumb,self.y_thumb,thumb_binary,)) + except sqlite3.Error,e: + _logger.debug('write thumbnail error %s'%e) + return None + self.db.commit() + return ret + + def position(self,x,y): + self.x = x + self.y = y + + def size(self, x_size, y_size): + self.size_x = x_size + self.size_y = y_size + + def set_border(self,b): + self.border = b + + def select(self): + self.surface.fill(selected_color) + self.thumbnail.blit(self.scaled,[0,0]) + screen.blit(self.surface,[self.x,self.y]) + return self + + def unselect(self): + self.surface.fill(background_color) + self.thumbnail.blit(self.scaled,[0,0]) + screen.blit(self.surface,[self.x,self.y]) + return self + + +class DisplayMany(): + """ + Receives array of rows (an album), and open database object refering to + database:'xophoto.sqlite' which is stored in the journal + """ + def __init__(self,rows,dbaccess,index=0): + self.rows = rows + self.db = dbaccess + self.pict_dict = {} + self.screen_width = 1000 + self.screen_height = 700 + self.screen_origin_x = 200 + self.screen_origin_y = 000 + self.pict_per_row = 6 + self.num_rows = 1 + self.display_start_index = index + self.origin_row = 0 + if index < 0: + self.display_start_index = 0 + self.origin_row = 0 + elif index >= len(rows): + self.display_start_index = len(rows) - self.pict_per_row + self.origin_row = index // self.pict_per_row + self.selected_index = index + self.last_selected = None + + def paint(self): + """ + Put multiple images on pygame screen. + Inputs: 1. cursor pointing to picture records of xophoto.sqlite + 2. Index into cursor + """ + #protect from an empty database + if len(self.rows) == 0: return + #figure out what size to paint, assuming square aspect ratio + if self.pict_per_row > 0: + #x_size = math.floor(self.screen_width/self.pict_per_row) + x_size = self.screen_width // self.pict_per_row + else: + raise PhotoException('pict_per_row was zero or negative') + if x_size > self.screen_width: + x_size = self.screen_width + if x_size > self.screen_height: + x_size = self.screen_height + y_size = x_size + num_pict = len(self.rows) + if num_pict > self.num_rows * self.pict_per_row: + num_pict = self.num_rows * self.pict_per_row + self.display_start_index = self.origin_row * self.pict_per_row + #check for upper bound on rows + if num_pict + self.display_start_index > len(self.rows): + num_pict = len(self.rows)-self.display_start_index + self.last_selected.unselect() + self.last_selected = None + screen.fill((255,255,255)) + _logger.debug('displaymany in range %s,%s'%(self.display_start_index, num_pict + self.display_start_index,)) + for i in range(self.display_start_index, num_pict + self.display_start_index): + if not self.pict_dict.has_key(i): + self.pict_dict[i] = DisplayOne(self.rows,self.db,i) + row = i // self.pict_per_row + pos_x = self.screen_origin_x + (i % self.pict_per_row) * x_size + pos_y = self.screen_origin_y + (row - self.origin_row) * y_size + self.pict_dict[i].position(pos_x,pos_y) + #_logger.debug('calling paint with size(%s,%s) and position(%s,%s)'%(x_size,y_size,pos_x,pos_y,)) + self.pict_dict[i].size(x_size,y_size) + self.pict_dict[i].paint() + self.select_pict(self.selected_index) + + def screen_width(self,width): + self.screen_width = width + + def screen_height(self,height): + self.screen_height = height + + def num_per_row(self,num): + self.pict_per_row = num + + def number_of_rows(self,num): + self.num_rows = num + + def select_pict(self,num): + if self.last_selected: + self.last_selected.unselect() + self.last_selected = self.pict_dict[num].select() + + def next(self): + if self.selected_index < len(self.rows)-1: + self.selected_index += 1 + #self.display_start_index = self.selected_index + if self.selected_index >= (self.origin_row + self.num_rows) * self.pict_per_row: + self.origin_row += 1 + self.paint() + self.select_pict(self.selected_index) + + def next_row(self): + if self.selected_index // self.pict_per_row < len(self.rows) // self.pict_per_row: + self.selected_index += self.pict_per_row + if self.selected_index > len(self.rows)-1: + self.selected_index = len(self.rows)-1 + if self.selected_index >= (self.origin_row + self.num_rows) * self.pict_per_row: + self.origin_row += 1 + self.paint() + self.select_pict(self.selected_index) + + + def prev(self): + if self.selected_index > 0: + self.selected_index -= 1 + if self.selected_index < (self.origin_row) * self.pict_per_row: + self.origin_row -= 1 + self.paint() + self.select_pict(self.selected_index) + + def prev_row(self): + if self.selected_index // self.pict_per_row > 0: + self.selected_index -= self.pict_per_row + if self.selected_index // self.pict_per_row < self.origin_row: + self.origin_row -= 1 + self.paint() + self.select_pict(self.selected_index) + +class DisplayAlbums(): + """Shows the photo albums on left side of main screen, responds to clicks, drag/drop events""" + + predefined_albums = [_('Journal'),_('Trash'),_('Duplicates'),_('Last Year'),_('Last Month'),] + def __init__(self): + self.num_of_last_rolls = 5 + self.background = (0,0,200) + self.album_height = 50 + + def one_album(self,album): + surf = pygame.Surface((self.album_height,100)) + font = pygame.font.Font(None,50) + text = font.render(album,0,self.background) + rect = text.get_rect() + surf.blit(text,(0,0)) + item = surf.get_rect() + _logger.debug('one album %s'%album) + return text + + def paint_albums(self): + for index in range(len(self.predefined_albums)): + screen.blit(self.one_album(self.predefined_albums[index]),(0,index*self.album_height)) + + def do_roll_over(self,x,y, in_drag=False): + """indicate willingness to be selected""" + pass + + def do_click(self,x,y): + """select the pointed to item""" + pass + + def do_drag_up(self,x,y): + """add the dragged item to the selected album""" + pass + + def add_new_album(self,name): + pass + + def delete_album(self,x,y): + pass + + +class Application(): + #how far does a drag need to be not to be ignored? + drag_threshold = 10 + + def run(self): + global screen + global in_click_delay + + self.db = DbAccess('xophoto.sqlite') + if not self.db.is_open(): + _logger.debug('filed to open "xophoto.sqlite" database') + return + """ + conn = sqlite3.connect('xophoto.sqlite') + #rows generated thusly will have columns that are addressable as dict of fieldnames + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + """ + if True: + + running = True + do_display = True + + pygame.init() + pygame.display.set_mode((0, 0), pygame.RESIZABLE) + screen = pygame.display.get_surface() + + while running: + # Pump GTK messages. + while gtk.events_pending(): + gtk.main_iteration() + + # Pump PyGame messages. + for event in pygame.event.get(): + if event.type in (MOUSEBUTTONDOWN,MOUSEBUTTONUP,MOUSEMOTION): + x,y = event.pos + if event.type == KEYUP: + print event + if event.key == K_ESCAPE: + running = False + pygame.quit() + elif event.key == K_LEFT: + dm.prev() + pygame.display.flip() + elif event.key == K_RIGHT: + dm.next() + pygame.display.flip() + elif event.key == K_UP: + dm.prev_row() + pygame.display.flip() + elif event.key == K_DOWN: + dm.next_row() + pygame.display.flip() + + #mouse events + elif event.type == MOUSEBUTTONDOWN: + if self.mouse_timer_running(): #this is a double click + self.process_mouse_double_click(x, y) + in_click_delay = False + else: #just a single click + self.process_mouse_click(x, y) + elif event.type == MOUSEMOTION: + self.drag(x, y) + elif event.type == MOUSEBUTTONUP: + if in_drag: + self.drop(x, y) + if event.type == pygame.QUIT: + return + + elif event.type == pygame.VIDEORESIZE: + pygame.display.set_mode(event.size, pygame.RESIZABLE) + + + if do_display: + do_display = False + # Clear Display + screen.fill((255,255,255)) #255 for white + #fetch the album (array of album records) + sql = 'select * from picture' + rows,cur = self.db.dbdo(sql) + dm = DisplayMany(rows,self.db) + dm.num_per_row(10) + dm.number_of_rows(5) + dm.paint() + albums = DisplayAlbums() + albums.paint_albums() + #albums.paint_albums() + + # Flip Display + pygame.display.flip() + else: + print('xophoto sqlite database failed to open') + + def drag(self,x,y): + global in_drag + l,m,r = pygame.mouse.get_pressed() + if not l: return + if not in_drag: + print('drag started at %s,%s'%(x,y,)) + in_drag = True + #record the initial position + self.drag_start_x,self.drag_start_y = x,y + + def drop(self,x,y): + #if the drag is less than threshold, ignore + if max(abs(self.drag_start_x - x), abs(self.drag_start_y - y)) < self.drag_threshold: + in_drag = False + return + print('drop at %s,%s'%(x,y,)) + pass + + def process_mouse_click(self,x,y): + print('mouse single click') + pass + + def process_mouse_double_click(self,x,y): + print('double click') + pass + + def mouse_timer_running(self): + global in_click_delay + if not in_click_delay: + Timer(0.5, self.end_delay, ()).start() + in_click_delay = True + return False + return True + + def end_delay(self): + global in_click_delay + in_click_delay = False + + + +if __name__ == '__main__': + ap = Application() + ap.run() + diff --git a/dist/XoPhoto-1.xo b/dist/XoPhoto-1.xo Binary files differnew file mode 100644 index 0000000..aec1622 --- /dev/null +++ b/dist/XoPhoto-1.xo diff --git a/photo_toolbar.py b/photo_toolbar.py new file mode 100644 index 0000000..964b679 --- /dev/null +++ b/photo_toolbar.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python +# +# Copyright (C) 2009, George Hunt <georgejhunt@gmail.com> +# +# 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 + +import gtk +import gobject +#import gconf + +from sugar.graphics.toolbox import Toolbox +from sugar.graphics.xocolor import XoColor +from sugar.graphics.icon import Icon +from sugar.graphics.toolcombobox import ToolComboBox +from sugar.graphics.toolbutton import ToolButton +from gettext import gettext as _ + +class ActivityToolbar(gtk.Toolbar): + """The Activity toolbar with the Journal entry title, sharing, + Keep and Stop buttons + + All activities should have this toolbar. It is easiest to add it to your + Activity by using the ActivityToolbox. + """ + def __init__(self, activity): + gtk.Toolbar.__init__(self) + + self._activity = activity + self._updating_share = False + """ + activity.connect('shared', self.__activity_shared_cb) + activity.connect('joined', self.__activity_shared_cb) + activity.connect('notify::max_participants', + self.__max_participants_changed_cb) + """ + #if activity.metadata: + if True: + self.title = gtk.Entry() + self.title.set_size_request(int(gtk.gdk.screen_width() / 6), -1) + if activity.metadata: + self.title.set_text(activity.metadata['title']) + activity.metadata.connect('updated', self.__jobject_updated_cb) + self.title.connect('changed', self.__title_changed_cb) + self._add_widget(self.title) + + self.add_album = ToolButton('list-add') + self.add_album.set_tooltip(_("Add Album")) + self.add_album.show() + self.insert(self.add_album,-1) + + self.delete_album = ToolButton('list-remove') + self.delete_album.set_tooltip(_("Remove Album")) + self.delete_album.show() + self.insert(self.delete_album,-1) + + + + """ + separator = gtk.SeparatorToolItem() + separator.props.draw = False + separator.set_expand(True) + self.insert(separator, -1) + separator.show() + + self.share = ToolComboBox(label_text=_('Traceback:')) + self.share.combo.connect('changed', self.__traceback_changed_cb) + self.share.combo.append_item("traceback_plain", _('Plain')) + self.share.combo.append_item('traceback_context', _('Context')) + self.share.combo.append_item('traceback_verbose', _('Verbose')) + self.insert(self.share, -1) + self.share.show() + + self._update_share() + """ + self.keep = ToolButton(tooltip=_('Keep')) + #client = gconf.client_get_default() + #color = XoColor(client.get_string('/desktop/sugar/user/color')) + #keep_icon = Icon(icon_name='document-save', xo_color=color) + keep_icon = Icon(icon_name='document-save') + self.keep.set_icon_widget(keep_icon) + keep_icon.show() + self.keep.props.accelerator = '<Ctrl>S' + self.keep.connect('clicked', self.__keep_clicked_cb) + self.insert(self.keep, -1) + self.keep.hide() + + separator = gtk.SeparatorToolItem() + separator.props.draw = False + separator.set_expand(True) + self.insert(separator, -1) + separator.show() + + self.stop = ToolButton('activity-stop', tooltip=_('Stop')) + self.stop.props.accelerator = '<Ctrl>Q' + self.stop.connect('clicked', self.__stop_clicked_cb) + self.insert(self.stop, -1) + self.stop.show() + + self._update_title_sid = None + + def _update_share(self): + self._updating_share = True + + if self._activity.props.max_participants == 1: + self.share.hide() + + if self._activity.get_shared(): + self.share.set_sensitive(False) + self.share.combo.set_active(1) + else: + self.share.set_sensitive(True) + self.share.combo.set_active(0) + + self._updating_share = False + + def __traceback_changed_cb(self, combo): + model = self.share.combo.get_model() + it = self.share.combo.get_active_iter() + (scope, ) = model.get(it, 0) + if scope == 'traceback_plain': + self._activity.traceback = 'Plain' + self._activity.debug_dict['traceback'] = 'plain' + elif scope == 'traceback_context': + self._activity.traceback = 'Context' + self._activity.debug_dict['traceback'] = 'context' + elif scope == 'traceback_verbose': + self._activity.traceback = 'Verbose' + self._activity.debug_dict['traceback'] = 'verbose' + self._activity.set_ipython_traceback() + + def __keep_clicked_cb(self, button): + self._activity.save_icon_clicked = True + self._activity.copy() + + def __stop_clicked_cb(self, button): + self._activity.close() + + def __jobject_updated_cb(self, jobject): + self.title.set_text(jobject['title']) + + def __title_changed_cb(self, entry): + if not self._update_title_sid: + self._update_title_sid = gobject.timeout_add_seconds( + 1, self.__update_title_cb) + + def __update_title_cb(self): + title = self.title.get_text() + + self._activity.metadata['title'] = title + self._activity.metadata['title_set_by_user'] = '1' + self._activity.save() + + shared_activity = self._activity.get_shared_activity() + if shared_activity: + shared_activity.props.name = title + + self._update_title_sid = None + return False + + def _add_widget(self, widget, expand=False): + tool_item = gtk.ToolItem() + tool_item.set_expand(expand) + + tool_item.add(widget) + widget.show() + + self.insert(tool_item, -1) + tool_item.show() + + def __activity_shared_cb(self, activity): + self._update_share() + + def __max_participants_changed_cb(self, activity, pspec): + self._update_share() + +class ActivityToolbox(Toolbox): + """Creates the Toolbox for the Activity + + By default, the toolbox contains only the ActivityToolbar. After creating + the toolbox, you can add your activity specific toolbars, for example the + EditToolbar. + + To add the ActivityToolbox to your Activity in MyActivity.__init__() do: + + # Create the Toolbar with the ActivityToolbar: + toolbox = activity.ActivityToolbox(self) + ... your code, inserting all other toolbars you need, like EditToolbar + + # Add the toolbox to the activity frame: + self.set_toolbox(toolbox) + # And make it visible: + toolbox.show() + """ + def __init__(self, activity): + Toolbox.__init__(self) + + self._activity_toolbar = ActivityToolbar(activity) + self.add_toolbar(_('Activity'), self._activity_toolbar) + self._activity_toolbar.show() + + def get_activity_toolbar(self): + return self._activity_toolbar + + diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..d3ab3a3 --- /dev/null +++ b/setup.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python + +# Copyright (C) 2006, Red Hat, Inc.
+# +# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+# +# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+# +# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ +from sugar.activity import bundlebuilder + +bundlebuilder.start() diff --git a/sources.py b/sources.py new file mode 100644 index 0000000..e936d70 --- /dev/null +++ b/sources.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python +# sources.py +# +# Copyright (C) 2010 George Hunt +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# +from gettext import gettext as _ + +from sugar.datastore import datastore +import sys, os +import gtk +import shutil +import sqlite3 + +from dbphoto import * + +#pick up activity globals +from xophotoactivity import * + + +import logging +_logger = logging.getLogger('xophoto') +_logger.setLevel(logging.DEBUG) +console_handler = logging.StreamHandler() +console_formatter = logging.Formatter('%(name)s %(levelname)s %(funcName)s: %(lineno)d||| %(message)s') +console_handler.setFormatter(console_formatter) +#_logger.addHandler(console_handler) + +class Datastore_SQLite(): + """class for interfacing between the Journal and an SQLite database""" + def __init__(self,database_access_object): + """receives an open dbaccess object (defined in dbphoto) """ + #self.db = dbaccess(fn) + self.db = database_access_object + + def ok(self): + if self.db.is_open():return True + return False + + def scan_images(self): + """ + returns a list of journal object ids that have mime_type equal to one + of the entries in mimetype table of xophoto database. + """ + rtn = [] + mime_list = self.db.get_mime_list() + (results,count) = datastore.find({}) + for f in results: + dict = f.get_metadata().get_dictionary() + if dict["mime_type"].find('jpg')>-1: + _logger.debug('found jpg: %s in mime_list %r'%(dict["mime_type"],mime_list,)) + if dict["mime_type"] in mime_list: + #record the id, file size, file date, in_ds + self.db.create_picture_record(f.object_id, f.get_file_path()) + rtn.append(f.object_id) + f.destroy() + self.db.commit() + _logger.debug('%s found in journal'%count) + return rtn + + + def get_filename_from_jobject_id(self, id): + try: + ds_obj = datastore.get(id) + except Exception,e: + _logger.debug('get filename from id error: %s'%e) + return None + if ds_obj: + fn = ds_obj.get_file_path() + ds_obj.destroy() + return(fn) + return None + +class filetree(): + def __init__(self,fn): + self.db = dbaccess(fn) + self.dialog = None + + def get_path(self): + _logger.debug('dialog to get user path for importing into journal') + if not self.dialog: + self.dialog = gtk.FileChooserDialog("Select Folder..", + None, + gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OPEN, gtk.RESPONSE_OK)) + else: + self.dialog.show_all() + self.dialog.set_default_response(gtk.RESPONSE_OK) + #self.dialog.set_current_folder(os.path.dirname(self.last_filename)) + + filter = gtk.FileFilter() + filter.set_name("All files") + filter.add_pattern("*") + self.dialog.add_filter(filter) + + filter = gtk.FileFilter() + filter.set_name("Pictures") + filter.add_pattern("*.png,*.jpg,*jpeg,*.gif") + self.dialog.add_filter(filter) + + response = self.dialog.run() + if response == gtk.RESPONSE_OK: + _logger.debug('%s selected'%self.dialog.get_filename() ) + fname = self.dialog.get_filename() + self.last_filename = fname + elif response == gtk.RESPONSE_CANCEL: + fname = None + _logger.debug( 'File chooseer closed, no files selected') + self.dialog.hide_all() + self.dialog.destroy() + self.dialog = None + return fname + + def copy_tree_to_ds(self,path): + added = 0 + if os.path.isdir(path): + files = os.listdir(path) + for f in files: + mtype = '' + chunks = f.split('.') + if len(chunks)>1: + ext = chunks[-1] + if ext == 'jpg' or ext == 'jpeg': + mtype = 'image/jpg' + elif ext == 'gif': + mtype = 'image/gif' + elif ext == 'png': + mtype = 'image/png' + if mtype == '': continue + fullname = os.path.join(path,f) + info = os.stat(fullname) + size = info.st_size + if self.db.check_in_ds(fullname,size): continue + ds = datastore.create() + ds.metadata['filename'] = fullname + ds.metadata['title'] = f + ds.metadata['mime_type'] = mtype + dest = os.path.join(os.environ['SUGAR_ACTIVITY_ROOT'],'instance',f) + shutil.copyfile(fullname,dest) + ds.set_file_path(dest) + datastore.write(ds,transfer_ownership=True) + self.db.update_picture(ds.object_id,fullname) + ds.destroy() + added += 1 + return added + return 0 + + def fill_ds(self): + path = self.get_path() + if path: + return self.copy_tree_to_ds(path) + + + +if __name__ == '__main__': + db = DbAccess('xophoto.sqlite') + if db.is_open(): + ds_sql = Datastore_SQLite(db) + imagelist = ds_sql.scan_images() + exit() + for i in imagelist: + print('\n%s'%ds.get_filename_from_jobject_id(i)) + ft = filetree('xophoto.sqlite') + #new = ft.fill_ds() + print('%s datastore records added'%new) + else: + print('xophoto sqlite database failed to open') diff --git a/sugargame/__init__.py b/sugargame/__init__.py new file mode 100644 index 0000000..7e49527 --- /dev/null +++ b/sugargame/__init__.py @@ -0,0 +1 @@ +__version__ = '1.0' diff --git a/sugargame/canvas.py b/sugargame/canvas.py new file mode 100644 index 0000000..cf99a13 --- /dev/null +++ b/sugargame/canvas.py @@ -0,0 +1,56 @@ +import os +import gtk +import gobject +import pygame +import event + +CANVAS = None + +class PygameCanvas(gtk.EventBox): + def __init__(self, mainwindow): + gtk.EventBox.__init__(self) + + global CANVAS + assert CANVAS == None, "Only one PygameCanvas can be created, ever." + CANVAS = self + + self._mainwindow = mainwindow + + self.set_flags(gtk.CAN_FOCUS) + + self._socket = gtk.Socket() + self.add(self._socket) + self.show_all() + + def run_pygame(self, main_fn): + # Run the main loop after a short delay. The reason for the delay is that the + # Sugar activity is not properly created until after its constructor returns. + # If the Pygame main loop is called from the activity constructor, the + # constructor never returns and the activity freezes. + gobject.idle_add(self._run_pygame_cb, main_fn) + + def _run_pygame_cb(self, main_fn): + assert pygame.display.get_surface() is None, "PygameCanvas.run_pygame can only be called once." + + # Preinitialize Pygame with the X window ID. + assert pygame.display.get_init() == False, "Pygame must not be initialized before calling PygameCanvas.run_pygame." + os.environ['SDL_WINDOWID'] = str(self._socket.get_id()) + pygame.init() + + # Restore the default cursor. + self._socket.get_window().set_cursor(None) + + # Initialize the Pygame window. + r = self.get_allocation() + pygame.display.set_mode((r.width, r.height), pygame.RESIZABLE) + + # Hook certain Pygame functions with GTK equivalents. + translator = event.Translator(self._mainwindow, self) + translator.hook_pygame() + + # Run the Pygame main loop. + main_fn() + return False + + def get_pygame_widget(self): + return self._socket diff --git a/sugargame/event.py b/sugargame/event.py new file mode 100644 index 0000000..52ca4ab --- /dev/null +++ b/sugargame/event.py @@ -0,0 +1,241 @@ +import gtk +import gobject +import pygame +import pygame.event +import logging + +class _MockEvent(object): + def __init__(self, keyval): + self.keyval = keyval + +class Translator(object): + key_trans = { + 'Alt_L': pygame.K_LALT, + 'Alt_R': pygame.K_RALT, + 'Control_L': pygame.K_LCTRL, + 'Control_R': pygame.K_RCTRL, + 'Shift_L': pygame.K_LSHIFT, + 'Shift_R': pygame.K_RSHIFT, + 'Super_L': pygame.K_LSUPER, + 'Super_R': pygame.K_RSUPER, + 'KP_Page_Up' : pygame.K_KP9, + 'KP_Page_Down' : pygame.K_KP3, + 'KP_End' : pygame.K_KP1, + 'KP_Home' : pygame.K_KP7, + 'KP_Up' : pygame.K_KP8, + 'KP_Down' : pygame.K_KP2, + 'KP_Left' : pygame.K_KP4, + 'KP_Right' : pygame.K_KP6, + + } + + mod_map = { + pygame.K_LALT: pygame.KMOD_LALT, + pygame.K_RALT: pygame.KMOD_RALT, + pygame.K_LCTRL: pygame.KMOD_LCTRL, + pygame.K_RCTRL: pygame.KMOD_RCTRL, + pygame.K_LSHIFT: pygame.KMOD_LSHIFT, + pygame.K_RSHIFT: pygame.KMOD_RSHIFT, + } + + def __init__(self, mainwindow, inner_evb): + """Initialise the Translator with the windows to which to listen""" + self._mainwindow = mainwindow + self._inner_evb = inner_evb + + # Enable events + self._mainwindow.set_events( + gtk.gdk.KEY_PRESS_MASK | \ + gtk.gdk.KEY_RELEASE_MASK \ + ) + + self._inner_evb.set_events( + gtk.gdk.POINTER_MOTION_MASK | \ + gtk.gdk.POINTER_MOTION_HINT_MASK | \ + gtk.gdk.BUTTON_MOTION_MASK | \ + gtk.gdk.BUTTON_PRESS_MASK | \ + gtk.gdk.BUTTON_RELEASE_MASK + ) + + self._mainwindow.set_flags(gtk.CAN_FOCUS) + self._inner_evb.set_flags(gtk.CAN_FOCUS) + + # Callback functions to link the event systems + self._mainwindow.connect('unrealize', self._quit_cb) + self._inner_evb.connect('key_press_event', self._keydown_cb) + self._inner_evb.connect('key_release_event', self._keyup_cb) + self._inner_evb.connect('button_press_event', self._mousedown_cb) + self._inner_evb.connect('button_release_event', self._mouseup_cb) + self._inner_evb.connect('motion-notify-event', self._mousemove_cb) + self._inner_evb.connect('expose-event', self._expose_cb) + self._inner_evb.connect('configure-event', self._resize_cb) + + # Internal data + self.__stopped = False + self.__keystate = [0] * 323 + self.__button_state = [0,0,0] + self.__mouse_pos = (0,0) + self.__repeat = (None, None) + self.__held = set() + self.__held_time_left = {} + self.__held_last_time = {} + self.__tick_id = None + + def hook_pygame(self): + pygame.key.get_pressed = self._get_pressed + pygame.key.set_repeat = self._set_repeat + pygame.mouse.get_pressed = self._get_mouse_pressed + pygame.mouse.get_pos = self._get_mouse_pos + + def _expose_cb(self, event, widget): + pygame.event.post(pygame.event.Event(pygame.VIDEOEXPOSE)) + return True + + def _resize_cb(self, widget, event): + evt = pygame.event.Event(pygame.VIDEORESIZE, + size=(event.width,event.height), width=event.width, height=event.height) + pygame.event.post(evt) + return False # continue processing + + def _quit_cb(self, data=None): + self.__stopped = True + pygame.event.post(pygame.event.Event(pygame.QUIT)) + + def _keydown_cb(self, widget, event): + key = event.keyval + if key in self.__held: + return True + else: + if self.__repeat[0] is not None: + self.__held_last_time[key] = pygame.time.get_ticks() + self.__held_time_left[key] = self.__repeat[0] + self.__held.add(key) + + return self._keyevent(widget, event, pygame.KEYDOWN) + + def _keyup_cb(self, widget, event): + key = event.keyval + if self.__repeat[0] is not None: + if key in self.__held: + # This is possibly false if set_repeat() is called with a key held + del self.__held_time_left[key] + del self.__held_last_time[key] + self.__held.discard(key) + + return self._keyevent(widget, event, pygame.KEYUP) + + def _keymods(self): + mod = 0 + for key_val, mod_val in self.mod_map.iteritems(): + mod |= self.__keystate[key_val] and mod_val + return mod + + def _keyevent(self, widget, event, type): + key = gtk.gdk.keyval_name(event.keyval) + if key is None: + # No idea what this key is. + return False + + keycode = None + if key in self.key_trans: + keycode = self.key_trans[key] + elif hasattr(pygame, 'K_'+key.upper()): + keycode = getattr(pygame, 'K_'+key.upper()) + elif hasattr(pygame, 'K_'+key.lower()): + keycode = getattr(pygame, 'K_'+key.lower()) + elif key == 'XF86Start': + # view source request, specially handled... + self._mainwindow.view_source() + else: + print 'Key %s unrecognized' % key + + if keycode is not None: + if type == pygame.KEYDOWN: + mod = self._keymods() + self.__keystate[keycode] = type == pygame.KEYDOWN + if type == pygame.KEYUP: + mod = self._keymods() + ukey = unichr(gtk.gdk.keyval_to_unicode(event.keyval)) + if ukey == '\000': + ukey = '' + evt = pygame.event.Event(type, key=keycode, unicode=ukey, mod=mod) + self._post(evt) + + return True + + def _get_pressed(self): + return self.__keystate + + def _get_mouse_pressed(self): + return self.__button_state + + def _mousedown_cb(self, widget, event): + self.__button_state[event.button-1] = 1 + return self._mouseevent(widget, event, pygame.MOUSEBUTTONDOWN) + + def _mouseup_cb(self, widget, event): + self.__button_state[event.button-1] = 0 + return self._mouseevent(widget, event, pygame.MOUSEBUTTONUP) + + def _mouseevent(self, widget, event, type): + evt = pygame.event.Event(type, button=event.button, pos=(event.x, event.y)) + self._post(evt) + return True + + def _mousemove_cb(self, widget, event): + # From http://www.learningpython.com/2006/07/25/writing-a-custom-widget-using-pygtk/ + # if this is a hint, then let's get all the necessary + # information, if not it's all we need. + if event.is_hint: + x, y, state = event.window.get_pointer() + else: + x = event.x + y = event.y + state = event.state + + rel = (x - self.__mouse_pos[0], y - self.__mouse_pos[1]) + self.__mouse_pos = (x, y) + + self.__button_state = [ + state & gtk.gdk.BUTTON1_MASK and 1 or 0, + state & gtk.gdk.BUTTON2_MASK and 1 or 0, + state & gtk.gdk.BUTTON3_MASK and 1 or 0, + ] + + evt = pygame.event.Event(pygame.MOUSEMOTION, + pos=self.__mouse_pos, rel=rel, buttons=self.__button_state) + self._post(evt) + return True + + def _tick_cb(self): + cur_time = pygame.time.get_ticks() + for key in self.__held: + delta = cur_time - self.__held_last_time[key] + self.__held_last_time[key] = cur_time + + self.__held_time_left[key] -= delta + if self.__held_time_left[key] <= 0: + self.__held_time_left[key] = self.__repeat[1] + self._keyevent(None, _MockEvent(key), pygame.KEYDOWN) + + return True + + def _set_repeat(self, delay=None, interval=None): + if delay is not None and self.__repeat[0] is None: + self.__tick_id = gobject.timeout_add(10, self._tick_cb) + elif delay is None and self.__repeat[0] is not None: + gobject.source_remove(self.__tick_id) + self.__repeat = (delay, interval) + + def _get_mouse_pos(self): + return self.__mouse_pos + + def _post(self, evt): + try: + pygame.event.post(evt) + except pygame.error, e: + if str(e) == 'Event queue full': + print "Event queue full!" + pass + else: + raise e diff --git a/xophoto.sqlite b/xophoto.sqlite Binary files differnew file mode 100644 index 0000000..8e1a1c8 --- /dev/null +++ b/xophoto.sqlite diff --git a/xophotoactivity.py b/xophotoactivity.py new file mode 100644 index 0000000..2a199e2 --- /dev/null +++ b/xophotoactivity.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python +# xophotoactivity.py +# +# Copyright (C) 2010 George Hunt +# +# 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 +# +""" +Top level description of the components of XoPhoto Activity +xophotoactivity.py --this file, subclasses Activity, Sets up Menus, canvas, + fetches the display module.Application class in display.py, + passes application object to sugargame.<canvas application runner> +display.py --contains the xophoto main loop this analyzes keystokes and + manipulates the display +sources.py --obtains images from datastore, folders, cameras and puts relevant + information into the sqlite database +sinks.py --combines information from the sqlite database and the Journal and + pumps it to various destinations, folders, email messages, slideshows +dbphoto.py --interfaces with the sqlite database, provides low level db access +sugarpygame --pygame platform developed sugarlabs.org see: + http://wiki.sugarlabs.org/go/Development_Team/sugargame +""" +from gettext import gettext as _ + +import gtk +import pygame +from sugar.activity import activity +from sugar.graphics.toolbutton import ToolButton +import gobject +import sugargame.canvas +import os +import shutil + +import display +import photo_toolbar + +#Application Globals +album_column_width = 200 + +import logging +_logger = logging.getLogger('xophoto') +_logger.setLevel(logging.DEBUG) +console_handler = logging.StreamHandler() +console_formatter = logging.Formatter('%(name)s %(levelname)s %(funcName)s: %(lineno)d||| %(message)s') +console_handler.setFormatter(console_formatter) +#_logger.addHandler(console_handler) + + +class XoPhotoActivity(activity.Activity): + def __init__(self, handle): + if handle and handle.object_id != '': + make_jobject = False + else: + make_jobject = True + activity.Activity.__init__(self, handle, create_jobject = make_jobject) + """ + #for development purposes, copy the sqlite database to the data directory + source = os.path.join(os.getcwd(),'xophoto.sqlite') + dest = os.path.join(os.environ['SUGAR_ACTIVITY_ROOT'],'data','xophoto.sqlite') + if handle.object_id == None and source != dest: + shutil.copy('./xophoto.sqlite',dest) + """ + # Build the activity toolbar. + self.build_toolbar() + + # Create the game instance. + self.game = display.Application() + + # Build the Pygame canvas. + self._pygamecanvas = sugargame.canvas.PygameCanvas(self) + # Note that set_canvas implicitly calls read_file when resuming from the Journal. + self.set_canvas(self._pygamecanvas) + + # Start the game running. + self._pygamecanvas.run_pygame(self.game.run) + + def build_toolbar(self): + toolbox = photo_toolbar.ActivityToolbox(self) + activity_toolbar = toolbox.get_activity_toolbar() + label = gtk.Label(_('New Album Name:')) + tool_item = gtk.ToolItem() + tool_item.set_expand(False) + tool_item.add(label) + label.show() + activity_toolbar.insert(tool_item, 0) + tool_item.show() + + activity_toolbar._add_widget(label) + activity_toolbar.keep.props.visible = False + #activity_toolbar.share.props.visible = False + + self.edit_toolbar = EditToolbar() + toolbox.add_toolbar(_('Edit'), self.edit_toolbar) + self.edit_toolbar.connect('do-import', + self.edit_toolbar_doimport_cb) + self.edit_toolbar.show() + + self.use_toolbar = UseToolbar() + toolbox.add_toolbar(_('Use'), self.use_toolbar) + self.use_toolbar.connect('do-export', + self.use_toolbar_doexport_cb) + self.use_toolbar.connect('do-upload', + self.use_toolbar_doupload_cb) + self.use_toolbar.connect('do-slideshow', + self.use_toolbar_doslideshow_cb) + self.use_toolbar.show() + + toolbox.show() + self.set_toolbox(toolbox) + + def edit_toolbar_doimport_cb(self, view_toolbar): + pass + + def use_toolbar_doexport_cb(self,use_toolbar): + pass + + def use_toolbar_doupload_cb(self,use_toolbar): + pass + + def use_toolbar_doslideshow_cb(self,use_toolbar): + pass + + def read_file(self, file_path): + sql_file = open(file_path, "rb") + local_path = os.path.join(os.environ['SUGAR_ACTIVITY_ROOT'],'data','xophoto.sqlite') + f = open(local_path, 'wb') + _logger.debug('reading from %s and writeing to %s'%(file_path,local_path,)) + try: + while sql_file: + block = sql_file.read(4096) + f.write(block) + except IOError, e: + _logger.debug('read sqlite file to local error %s'%e) + finally: + f.close + sql_file.close() + + def write_file(self, file_path): + try: + local_path = os.path.join(os.environ['SUGAR_ACTIVITY_ROOT'],'data','xophoto.sqlite') + self.metadata['filename'] = local_path + self.metadata['mime_type'] = mtype + #dest = os.path.join(os.environ['SUGAR_ACTIVITY_ROOT'],'instance',f) + _logger.debug('write_file %s to %s'%(local_path,file_path,)) + shutil.copyfile(local_path,file_path) + self.set_file_path(dest) + except Exception,e: + _logger.debug('write_file exception %s'%e) + raise e + +class EditToolbar(gtk.Toolbar): + __gtype_name__ = 'EditToolbar' + + __gsignals__ = { + 'needs-update-size': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'do-import': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])) + } + + def __init__(self): + gtk.Toolbar.__init__(self) + self.doimport = ToolButton() + self.doimport.set_stock_id('gtk-open') + self.doimport.set_icon_widget(None) + self.doimport.set_tooltip(_('Import from SD or USB')) + self.doimport.connect('clicked', self.doimport_cb) + self.insert(self.doimport, -1) + self.doimport.show() + + self.delete_comment = ToolButton() + self.delete_comment.set_stock_id('gtk_stock_delete') + self.delete_comment.set_tooltip(_("Remove Picture")) + self.delete_comment.show() + self.insert(self.delete_comment,-1) + + separator = gtk.SeparatorToolItem() + separator.props.draw = False + separator.set_expand(True) + self.insert(separator, -1) + separator.show() + + self.entry = gtk.Entry() + self.entry.set_width_chars(50) + tool_item = gtk.ToolItem() + tool_item.set_expand(False) + tool_item.add(self.entry) + self.entry.show() + self.insert(tool_item, -1) + tool_item.show() + + self.add_comment = ToolButton('list-add') + self.add_comment.set_tooltip(_("Add Annotation")) + self.add_comment.show() + self.insert(self.add_comment,-1) + + self.stop = ToolButton('activity-stop', tooltip=_('Stop')) + self.stop.props.accelerator = '<Ctrl>Q' + #self.stop.connect('clicked', self.__stop_clicked_cb) + self.insert(self.stop, -1) + self.stop.show() + + + def doimport_cb(self, button): + self.emit('do-import') + +class UseToolbar(gtk.Toolbar): + __gtype_name__ = 'UseToolbar' + + __gsignals__ = { + 'do-export': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'do-upload': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'do-slideshow': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])) + } + + def __init__(self): + gtk.Toolbar.__init__(self) + self.doexport = ToolButton('view-fullscreen') + self.doexport.set_tooltip(_('Export to USB/SD/FileSystem')) + self.doexport.connect('clicked', self.doexport_cb) + self.insert(self.doexport, -1) + self.doexport.show() + + separator = gtk.SeparatorToolItem() + separator.props.draw = False + separator.set_expand(True) + self.insert(separator, -1) + separator.show() + + self.doupload = ToolButton('view-fullscreen') + self.doupload.set_tooltip(_('Fullscreen')) + self.doupload.connect('clicked', self.doupload_cb) + self.insert(self.doupload, -1) + self.doupload.show() + + separator = gtk.SeparatorToolItem() + separator.props.draw = False + separator.set_expand(True) + self.insert(separator, -1) + separator.show() + + self.doslideshow = ToolButton() + self.doslideshow.set_stock_id('gtk-fullscreen') + self.doslideshow.set_tooltip(_('SlideShow')) + self.doslideshow.connect('clicked', self.doslideshow_cb) + self.insert(self.doslideshow, -1) + self.doslideshow.show() + + separator = gtk.SeparatorToolItem() + separator.props.draw = False + separator.set_expand(True) + self.insert(separator, -1) + separator.show() + + self.stop = ToolButton('activity-stop', tooltip=_('Stop')) + self.stop.props.accelerator = '<Ctrl>Q' + #self.stop.connect('clicked', self.__stop_clicked_cb) + self.insert(self.stop, -1) + self.stop.show() + + def doexport_cb(self, button): + self.emit('do-import') + + def doupload_cb(self, button): + self.emit('do-upload') + + def doslideshow_cb(self, button): + self.emit('do-slideshow') + |