Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorCruz Antonio Cardenas Gomez <cardenas.cruz@gmail.com>2012-03-30 17:22:39 (GMT)
committer Cruz Antonio Cardenas Gomez <cardenas.cruz@gmail.com>2012-03-30 17:22:39 (GMT)
commitc8480ef07eec6c3df4935ad3e02834eaf2d24b45 (patch)
tree5769f14a605c457ec9e00f588a2796dc35e3fd2a
second commit
-rw-r--r--MANIFEST36
-rw-r--r--__init__.py0
-rw-r--r--activity.py338
-rw-r--r--activity/activity-gtktest.svg549
-rw-r--r--activity/activity.info7
-rw-r--r--button.py71
-rw-r--r--button.pycbin0 -> 3505 bytes
-rw-r--r--collab.py339
-rw-r--r--collab.pycbin0 -> 12023 bytes
-rw-r--r--constants.py46
-rw-r--r--constants.pycbin0 -> 1082 bytes
-rw-r--r--gfx/corner-info.svg15
-rw-r--r--gfx/max-enlarge.svg21
-rw-r--r--gfx/max-reduce.svg22
-rw-r--r--gfx/media-circle.pngbin0 -> 394 bytes
-rw-r--r--gfx/media-insensitive.pngbin0 -> 435 bytes
-rw-r--r--gfx/media-pause.pngbin0 -> 405 bytes
-rw-r--r--gfx/media-play.pngbin0 -> 461 bytes
-rw-r--r--gfx/media-record-red.pngbin0 -> 467 bytes
-rw-r--r--gfx/media-record.pngbin0 -> 426 bytes
-rw-r--r--gfx/object-audio.svg10
-rw-r--r--gfx/object-photo.svg8
-rw-r--r--gfx/object-video.svg14
-rw-r--r--gfx/photoShutter.wavbin0 -> 460 bytes
-rw-r--r--gfx/xo-guy.svg17
-rw-r--r--glive.py645
-rw-r--r--glive.pycbin0 -> 20804 bytes
-rw-r--r--gplay.py116
-rw-r--r--gplay.pycbin0 -> 4530 bytes
-rw-r--r--instance.py22
-rw-r--r--instance.pycbin0 -> 1160 bytes
-rw-r--r--liitle-tramp.e4p58
-rw-r--r--mediaview.py515
-rw-r--r--mediaview.pycbin0 -> 20971 bytes
-rw-r--r--model.py403
-rw-r--r--model.pycbin0 -> 13541 bytes
-rw-r--r--record.py934
-rw-r--r--recorded.py180
-rw-r--r--recorded.pycbin0 -> 4700 bytes
-rw-r--r--serialize.py297
-rw-r--r--serialize.pycbin0 -> 8382 bytes
-rwxr-xr-xsetup.py12
-rw-r--r--tray.py202
-rw-r--r--tray.pycbin0 -> 7853 bytes
-rw-r--r--treeview.py55
-rw-r--r--treeview.pycbin0 -> 1322 bytes
-rw-r--r--utils.py56
-rw-r--r--utils.pycbin0 -> 2611 bytes
48 files changed, 4988 insertions, 0 deletions
diff --git a/MANIFEST b/MANIFEST
new file mode 100644
index 0000000..37987e9
--- /dev/null
+++ b/MANIFEST
@@ -0,0 +1,36 @@
+gtktest.activity/
+gtktest.activity/activity
+gtktest.activity/activity/activity-gtktest.svg
+gtktest.activity/activity/activity.info
+gtktest.activity/setup.py
+gtktest.activity/activity.py
+gtktest.activity/gtktest.py
+gtktest.activity/MANIFEST
+gtktest.activity/gtktest.xo
+gtktest.activity/gtktest.glade
+gtktest.activity/constants.py
+gtktest.activity/mediaview.py
+gtktest.activity/utils.py
+gtktest.activity/treeview.py
+gtktest.activity/glive.py
+gtktest.activity/gplay.py
+gtktest.activity/instance.py
+gtktest.activity/serialize.py
+gtktest.activity/model.py
+gtktest.activity/recorded.py
+gtktest.activity/tray.py
+gfx/media-pause.png
+gfx/object-photo.svg
+gfx/media-play.png
+gfx/photoShutter.wav
+gfx/corner-info.svg
+gfx/object-video.svg
+gfx/xo-guy.svg
+gfx/max-enlarge.svg
+gfx/max-reduce.svg
+gfx/media-record-red.png
+gfx/object-audio.svg
+gfx/media-record.png
+gfx/media-insensitive.png
+gfx/media-circle.png
+
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/__init__.py
diff --git a/activity.py b/activity.py
new file mode 100644
index 0000000..65ed37d
--- /dev/null
+++ b/activity.py
@@ -0,0 +1,338 @@
+import pygst
+pygst.require("0.10")
+# Load GTK
+import gtk
+from gtk import gdk
+import pygst
+import os, sys
+import time
+import gobject
+gobject.threads_init()
+from mediaview import MediaView
+import constants
+import gst
+import pygtk
+from treeview import TreeView
+from model import Model
+from button import RecdButton
+import utils
+from instance import Instance
+from tray import HTray
+import shutil
+# Load sugar libraries
+from sugar.activity import activity
+import logging
+
+COLOR_BLACK = gdk.color_parse('#000000')
+COLOR_WHITE = gdk.color_parse('#ffffff')
+
+class littleTrampActivity(activity.Activity):
+
+ def callback(self, widget, data=None):
+ print "Hello again"
+
+ def __init__(self, handle):
+ activity.Activity.__init__(self, handle)
+ self._name = handle
+
+ # Set title for our Activity
+ self.set_title('Little Tramp')
+
+ # Attach sugar toolbox (Share, ...)
+ #toolbox = activity.ActivityToolbox(self)
+ #self.set_toolbox(toolbox)
+ #toolbox.show()
+ Instance(self)
+
+ #self.set_canvas(self._main_view)
+ self.show_all()
+ self.model = Model(self)
+ self.ui_init()
+ self._media_view.realize_video()
+
+ # Changing to the first toolbar kicks off the rest of the setup
+ if self.model.get_has_camera():
+ self.model.change_mode(constants.MODE_PHOTO)
+ #else:
+ #self.model.change_mode(constants.MODE_AUDIO)
+
+ def _shutter_clicked(self, arg):
+ self.model.do_shutter()
+
+ def _remove_thumbnail(self, recdbutton):
+ handlers = recdbutton.get_data('handler-ids')
+ for handler in handlers:
+ recdbutton.disconnect(handler)
+
+ self._thumb_tray.remove_item(recdbutton)
+ recdbutton.cleanup()
+
+ def remove_all_thumbnails(self):
+ for child in self._thumb_tray.get_children():
+ self._remove_thumbnail(child)
+
+ def add_thumbnail(self, recd, scroll_to_end):
+ button = RecdButton(recd)
+ clicked_handler = button.connect("clicked", self._thumbnail_clicked, recd)
+ remove_handler = button.connect("remove-requested", self._remove_recd)
+ clipboard_handler = button.connect("copy-clipboard-requested", self._thumbnail_copy_clipboard)
+ button.set_data('handler-ids', (clicked_handler, remove_handler, clipboard_handler))
+ self._thumb_tray.add_item(button)
+ button.show()
+ if scroll_to_end:
+ self._thumb_tray.scroll_to_end()
+
+
+ def ui_init(self):
+ self._fullscreen = False
+ self._showing_info = False
+
+ # FIXME: if _thumb_tray becomes some kind of button group, we wouldn't
+ # have to track which recd is active
+ self._active_recd = None
+ toolbox = activity.ActivityToolbox(self)
+ activity_toolbar = toolbox.get_activity_toolbar()
+ activity_toolbar.keep.props.visible = False
+ activity_toolbar.share.props.visible = False
+ self.set_toolbox(toolbox)
+ toolbox.show()
+
+ self.tView = TreeView()
+
+ main_box = gtk.VBox()
+ self.set_canvas(main_box)
+ main_box.get_parent().modify_bg(gtk.STATE_NORMAL, COLOR_BLACK)
+ main_box.show()
+
+ self._media_view = MediaView()
+ #self._media_view.connect('media-clicked', self._media_view_media_clicked)
+ #self._media_view.connect('pip-clicked', self._media_view_pip_clicked)
+ #self._media_view.connect('info-clicked', self._media_view_info_clicked)
+ #self._media_view.connect('full-clicked', self._media_view_full_clicked)
+ #self._media_view.connect('tags-changed', self._media_view_tags_changed)
+ self._media_view.show()
+
+ self._controls_hbox = gtk.HBox()
+ self._controls_hbox.show()
+ self._treeview_vbox = gtk.VBox()
+ self._treeview_vbox.show()
+ self._treeview_vbox.pack_start(self.tView)
+ self._shutter_button = ShutterButton()
+ self._shutter_button.connect("clicked", self._shutter_clicked)
+ self._controls_hbox.pack_start(self._shutter_button, expand=True, fill=False)
+ self._shutter_button.show()
+
+ self._title_label = gtk.Label()
+ self._title_label.set_markup("<b><span foreground='white'>"+('Title:')+'</span></b>')
+ self._controls_hbox.pack_start(self._title_label, expand=False)
+
+ self._title_entry = gtk.Entry()
+ self._title_entry.modify_bg(gtk.STATE_INSENSITIVE, COLOR_BLACK)
+ self._title_entry.connect('changed', self._title_changed)
+ self._controls_hbox.pack_start(self._title_entry, expand=True, fill=True, padding=10)
+
+ container = RecordContainer(self._media_view, self._controls_hbox)
+ main_box.pack_start(self._treeview_vbox, expand=True)
+ main_box.pack_start(container, expand=True, fill=True, padding=6)
+ container.show()
+
+ self._thumb_tray = HTray()
+ self._thumb_tray.set_size_request(-1, 150)
+ main_box.pack_end(self._thumb_tray, expand=False)
+ self._thumb_tray.show_all()
+
+ def set_glive_sink(self, sink):
+ return self._media_view.set_video_sink(sink)
+
+ # can be called from gstreamer thread, so must not do any GTK+ stuff
+ def set_gplay_sink(self, sink):
+ return self._media_view.set_video2_sink(sink)
+
+ def _title_changed(self, widget):
+ self._active_recd.setTitle(self._title_entry.get_text())
+
+ def _thumbnail_clicked(self, button, recd):
+ if self.model.ui_frozen():
+ return
+
+ self._active_recd = recd
+ self._show_recd(recd)
+
+ def _show_recd(self, recd, play=True):
+ self._showing_info = False
+
+ def _remove_recd(self, recdbutton):
+ recd = recdbutton.get_recd()
+ self.model.delete_recd(recd)
+ if self._active_recd == recd:
+ self.model.set_state(constants.STATE_READY)
+
+ self._remove_thumbnail(recdbutton)
+
+ def _thumbnail_copy_clipboard(self, recdbutton):
+ self._copy_to_clipboard(recdbutton.get_recd())
+
+
+ def _copy_to_clipboard(self, recd):
+ if recd == None:
+ return
+ if not recd.isClipboardCopyable():
+ return
+
+ media_path = recd.getMediaFilepath()
+ tmp_path = utils.getUniqueFilepath(media_path, 0)
+ shutil.copyfile(media_path, tmp_path)
+ gtk.Clipboard().set_with_data([('text/uri-list', 0, 0)], self._clipboard_get, self._clipboard_clear, tmp_path)
+
+ def set_shutter_sensitive(self, value):
+ self._shutter_button.set_sensitive(value)
+
+
+class ShutterButton(gtk.Button):
+ def __init__(self):
+ gtk.Button.__init__(self)
+ self.set_relief(gtk.RELIEF_NONE)
+ self.set_focus_on_click(False)
+ self.modify_bg(gtk.STATE_ACTIVE, COLOR_BLACK)
+
+ path = os.path.join(constants.GFX_PATH, 'media-record.png')
+ self._rec_image = gtk.image_new_from_file(path)
+
+ path = os.path.join(constants.GFX_PATH, 'media-record-red.png')
+ self._rec_red_image = gtk.image_new_from_file(path)
+
+ path = os.path.join(constants.GFX_PATH, 'media-insensitive.png')
+ self._insensitive_image = gtk.image_new_from_file(path)
+
+ self.set_normal()
+
+ def set_sensitive(self, sensitive):
+ if sensitive:
+ self.set_image(self._rec_image)
+ else:
+ self.set_image(self._insensitive_image)
+ super(ShutterButton, self).set_sensitive(sensitive)
+
+ def set_normal(self):
+ self.set_image(self._rec_image)
+
+ def set_recording(self):
+ self.set_image(self._rec_red_image)
+class RecordContainer(gtk.Container):
+ """
+ A custom Container that contains a media view area, and a controls hbox.
+
+ The controls hbox is given the first height that it requests, locked in
+ for the duration of the widget.
+ The media view is given the remainder of the space, but is constrained to
+ a strict 4:3 ratio, therefore deducing its width.
+ The controls hbox is given the same width, and both elements are centered
+ horizontall.y
+ """
+ __gtype_name__ = 'RecordContainer'
+
+ def __init__(self, media_view, controls_hbox):
+ self._media_view = media_view
+ self._controls_hbox = controls_hbox
+ self._controls_hbox_height = 0
+ super(RecordContainer, self).__init__()
+
+ for widget in (self._media_view, self._controls_hbox):
+ if widget.flags() & gtk.REALIZED:
+ widget.set_parent_window(self.window)
+
+ widget.set_parent(self)
+
+ def do_realize(self):
+ self.set_flags(gtk.REALIZED)
+
+ self.window = gdk.Window(
+ self.get_parent_window(),
+ window_type=gdk.WINDOW_CHILD,
+ x=self.allocation.x,
+ y=self.allocation.y,
+ width=self.allocation.width,
+ height=self.allocation.height,
+ wclass=gdk.INPUT_OUTPUT,
+ colormap=self.get_colormap(),
+ event_mask=self.get_events() | gdk.VISIBILITY_NOTIFY_MASK | gdk.EXPOSURE_MASK)
+ self.window.set_user_data(self)
+
+ self.set_style(self.style.attach(self.window))
+
+ for widget in (self._media_view, self._controls_hbox):
+ widget.set_parent_window(self.window)
+ self.queue_resize()
+
+ # GTK+ contains on exit if remove is not implemented
+ def do_remove(self, widget):
+ pass
+
+ def do_size_request(self, req):
+ # always request 320x240 (as a minimum for video)
+ req.width = 320
+ req.height = 240
+
+ self._media_view.size_request()
+
+ w, h = self._controls_hbox.size_request()
+
+ # add on height requested by controls hbox
+ if self._controls_hbox_height == 0:
+ self._controls_hbox_height = h
+
+ req.height += self._controls_hbox_height
+
+ @staticmethod
+ def _constrain_4_3(width, height):
+ if (width % 4 == 0) and (height % 3 == 0) and ((width / 4) * 3) == height:
+ return width, height # nothing to do
+
+ ratio = 4.0 / 3.0
+ if ratio * height > width:
+ width = (width / 4) * 4
+ height = int(width / ratio)
+ else:
+ height = (height / 3) * 3
+ width = int(ratio * height)
+
+ return width, height
+
+ @staticmethod
+ def _center_in_plane(plane_size, size):
+ return (plane_size - size) / 2
+
+ def do_size_allocate(self, allocation):
+ self.allocation = allocation
+
+ # give the controls hbox the height that it requested
+ remaining_height = self.allocation.height - self._controls_hbox_height
+
+ # give the mediaview the rest, constrained to 4/3 and centered
+ media_view_width, media_view_height = self._constrain_4_3(self.allocation.width, remaining_height)
+ media_view_x = self._center_in_plane(self.allocation.width, media_view_width)
+ media_view_y = self._center_in_plane(remaining_height, media_view_height)
+
+ # send allocation to mediaview
+ alloc = gdk.Rectangle()
+ alloc.width = media_view_width
+ alloc.height = media_view_height
+ alloc.x = media_view_x
+ alloc.y = media_view_y
+ self._media_view.size_allocate(alloc)
+
+ # position hbox at the bottom of the window, with the requested height,
+ # and the same width as the media view
+ alloc = gdk.Rectangle()
+ alloc.x = media_view_x
+ alloc.y = self.allocation.height - self._controls_hbox_height
+ alloc.width = media_view_width
+ alloc.height = self._controls_hbox_height
+ self._controls_hbox.size_allocate(alloc)
+
+ if self.flags() & gtk.REALIZED:
+ self.window.move_resize(*allocation)
+
+ def do_forall(self, include_internals, callback, data):
+ for widget in (self._media_view, self._controls_hbox):
+ callback(widget, data)
diff --git a/activity/activity-gtktest.svg b/activity/activity-gtktest.svg
new file mode 100644
index 0000000..3aa716c
--- /dev/null
+++ b/activity/activity-gtktest.svg
@@ -0,0 +1,549 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
+"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+ version="1.0"
+ x="0"
+ y="0"
+ width="48pt"
+ height="48pt"
+ id="svg611"
+ sodipodi:version="0.32"
+ inkscape:version="0.38.1"
+ sodipodi:docname="larry.svg"
+ sodipodi:docbase="/home/scaba/files/lila-artwork/devel"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="5.6568542"
+ inkscape:cx="23.803488"
+ inkscape:cy="27.503542"
+ inkscape:window-width="498"
+ inkscape:window-height="498"
+ inkscape:window-x="301"
+ inkscape:window-y="94"
+ showguides="true"
+ snaptoguides="true"
+ showgrid="false"
+ snaptogrid="false" />
+ <defs
+ id="defs613">
+ <linearGradient
+ id="linearGradient1535">
+ <stop
+ style="stop-color:#ffffff;stop-opacity:1;"
+ offset="0"
+ id="stop1537" />
+ <stop
+ style="stop-color:#ffffff;stop-opacity:0;"
+ offset="1"
+ id="stop1536" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient1528">
+ <stop
+ style="stop-color:#d9cce6;stop-opacity:1.0000000;"
+ offset="0.0000000"
+ id="stop1530" />
+ <stop
+ style="stop-color:#73667f;stop-opacity:1.0000000;"
+ offset="1.0000000"
+ id="stop1529" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient642">
+ <stop
+ style="stop-color:#ffffff;stop-opacity:1;"
+ offset="0"
+ id="stop643" />
+ <stop
+ style="stop-color:#ffffff;stop-opacity:0;"
+ offset="1"
+ id="stop644" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient635">
+ <stop
+ style="stop-color:#ffffff;stop-opacity:1.0000000;"
+ offset="0.0000000"
+ id="stop636" />
+ <stop
+ style="stop-color:#d9cce6;stop-opacity:1.0000000;"
+ offset="1.0000000"
+ id="stop637" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient638"
+ xlink:href="#linearGradient635"
+ x1="6.5680968e-16"
+ y1="0.50000000"
+ x2="1.0000000"
+ y2="0.50000000" />
+ <linearGradient
+ id="linearGradient641"
+ xlink:href="#linearGradient1535"
+ x1="-6.5052130e-18"
+ y1="0.50000000"
+ x2="1.0000000"
+ y2="0.50000000" />
+ <linearGradient
+ id="linearGradient1522"
+ xlink:href="#linearGradient635"
+ x1="3.3989738e-17"
+ y1="0.50000000"
+ x2="1.0000000"
+ y2="0.50000000" />
+ <linearGradient
+ id="linearGradient1527"
+ xlink:href="#linearGradient635"
+ x1="0.0034014429"
+ y1="0.042553201"
+ x2="0.98639452"
+ y2="0.95744646" />
+ <linearGradient
+ id="linearGradient1532"
+ xlink:href="#linearGradient635"
+ x1="0.010615340"
+ y1="0.033530705"
+ x2="1.0544963"
+ y2="0.95004517"
+ gradientTransform="scale(0.961222,1.040342)" />
+ <linearGradient
+ id="linearGradient1534"
+ xlink:href="#linearGradient635"
+ x1="0.020918185"
+ y1="0.044817142"
+ x2="0.34863889"
+ y2="2.7786629"
+ gradientTransform="scale(2.868297,0.348639)" />
+ <linearGradient
+ id="linearGradient1539"
+ xlink:href="#linearGradient635"
+ x1="0.024142403"
+ y1="-8.7577068e-10"
+ x2="0.38024315"
+ y2="2.4907429"
+ gradientTransform="scale(2.510355,0.398350)" />
+ <linearGradient
+ id="linearGradient1567"
+ xlink:href="#linearGradient635"
+ x1="0.0040558549"
+ y1="-5.9399252e-07"
+ x2="1.1843110"
+ y2="0.81321537"
+ gradientTransform="scale(0.838629,1.192423)" />
+ <linearGradient
+ xlink:href="#linearGradient635"
+ id="linearGradient896"
+ x1="0.0099245850"
+ y1="0.031473983"
+ x2="0.95937836"
+ y2="0.97570312"
+ gradientTransform="scale(1.028160,0.972611)" />
+ <linearGradient
+ xlink:href="#linearGradient1528"
+ id="linearGradient899"
+ x1="0.041376498"
+ y1="0.0078670830"
+ x2="0.95856202"
+ y2="0.99913907"
+ gradientTransform="scale(1.007006,0.993043)" />
+ <linearGradient
+ xlink:href="#linearGradient1535"
+ id="linearGradient901" />
+ <linearGradient
+ xlink:href="#linearGradient1535"
+ id="linearGradient905"
+ x1="0.0048523550"
+ y1="-0.097928792"
+ x2="0.99029034"
+ y2="1.0497539" />
+ <linearGradient
+ xlink:href="#linearGradient1535"
+ id="linearGradient939"
+ x1="0.0041322312"
+ y1="0.023437500"
+ x2="0.98347110"
+ y2="0.97656250" />
+ <linearGradient
+ xlink:href="#linearGradient1535"
+ id="linearGradient948" />
+ <radialGradient
+ xlink:href="#linearGradient1535"
+ id="radialGradient921"
+ cx="0.50000000"
+ cy="0.50000000"
+ r="0.50000000"
+ fx="0.50000000"
+ fy="0.50000000" />
+ <linearGradient
+ xlink:href="#linearGradient1535"
+ id="linearGradient1136" />
+ <linearGradient
+ xlink:href="#linearGradient1535"
+ id="linearGradient1138" />
+ <linearGradient
+ xlink:href="#linearGradient1535"
+ id="linearGradient1140" />
+ <linearGradient
+ xlink:href="#linearGradient1535"
+ id="linearGradient1142" />
+ <linearGradient
+ xlink:href="#linearGradient1535"
+ id="linearGradient1144" />
+ <linearGradient
+ xlink:href="#linearGradient1535"
+ id="linearGradient1146" />
+ <linearGradient
+ xlink:href="#linearGradient1535"
+ id="linearGradient1148" />
+ <linearGradient
+ xlink:href="#linearGradient1535"
+ id="linearGradient1150" />
+ <linearGradient
+ xlink:href="#linearGradient1535"
+ id="linearGradient1152" />
+ <linearGradient
+ xlink:href="#linearGradient1535"
+ id="linearGradient1154" />
+ <linearGradient
+ xlink:href="#linearGradient1535"
+ id="linearGradient1156" />
+ <linearGradient
+ xlink:href="#linearGradient1535"
+ id="linearGradient1158" />
+ <linearGradient
+ xlink:href="#linearGradient1535"
+ id="linearGradient1160" />
+ <linearGradient
+ xlink:href="#linearGradient1535"
+ id="linearGradient1162" />
+ <linearGradient
+ xlink:href="#linearGradient1535"
+ id="linearGradient1165" />
+ <linearGradient
+ xlink:href="#linearGradient1528"
+ id="linearGradient1167"
+ x1="-8.7603536e-16"
+ y1="0.50000000"
+ x2="1.0000000"
+ y2="0.50000000" />
+ <linearGradient
+ xlink:href="#linearGradient1528"
+ id="linearGradient1169"
+ x1="1.9428903e-16"
+ y1="0.50000000"
+ x2="1.0000000"
+ y2="0.50000000" />
+ <linearGradient
+ xlink:href="#linearGradient1528"
+ id="linearGradient1171"
+ x1="2.9004577e-15"
+ y1="0.50000000"
+ x2="1.0000000"
+ y2="0.50000000" />
+ <linearGradient
+ xlink:href="#linearGradient1528"
+ id="linearGradient1173"
+ x1="7.4593109e-16"
+ y1="0.50000000"
+ x2="1.0000000"
+ y2="0.50000000" />
+ <linearGradient
+ xlink:href="#linearGradient1528"
+ id="linearGradient1175"
+ x1="1.2507356e-15"
+ y1="0.50000000"
+ x2="1.0000000"
+ y2="0.50000000" />
+ <linearGradient
+ xlink:href="#linearGradient1528"
+ id="linearGradient1177"
+ x1="1.4398205e-16"
+ y1="0.50000000"
+ x2="1.0000000"
+ y2="0.50000000" />
+ <linearGradient
+ xlink:href="#linearGradient1528"
+ id="linearGradient1179"
+ x1="2.5396352e-15"
+ y1="0.50000000"
+ x2="1.0000000"
+ y2="0.50000000" />
+ <linearGradient
+ xlink:href="#linearGradient1528"
+ id="linearGradient1181"
+ x1="-9.5236319e-16"
+ y1="0.50000000"
+ x2="1.0000000"
+ y2="0.50000000" />
+ <linearGradient
+ xlink:href="#linearGradient1528"
+ id="linearGradient1183"
+ x1="-1.5404344e-15"
+ y1="0.50000000"
+ x2="1.0000000"
+ y2="0.50000000" />
+ <linearGradient
+ xlink:href="#linearGradient1528"
+ id="linearGradient1185"
+ x1="1.3166551e-15"
+ y1="0.50000000"
+ x2="1.0000000"
+ y2="0.50000000" />
+ <linearGradient
+ xlink:href="#linearGradient1528"
+ id="linearGradient1187"
+ x1="-1.1518564e-15"
+ y1="0.50000000"
+ x2="1.0000000"
+ y2="0.50000000" />
+ <linearGradient
+ xlink:href="#linearGradient1528"
+ id="linearGradient1189"
+ x1="2.3019781e-15"
+ y1="0.50000000"
+ x2="1.0000000"
+ y2="0.50000000" />
+ <linearGradient
+ xlink:href="#linearGradient1528"
+ id="linearGradient1191"
+ x1="1.0633855e-15"
+ y1="0.50000000"
+ x2="1.0000000"
+ y2="0.50000000" />
+ <linearGradient
+ xlink:href="#linearGradient1528"
+ id="linearGradient1193"
+ x1="-1.5959456e-15"
+ y1="0.50000000"
+ x2="1.0000000"
+ y2="0.50000000" />
+ <linearGradient
+ xlink:href="#linearGradient1528"
+ id="linearGradient1195"
+ x1="-2.7183117e-15"
+ y1="0.50000000"
+ x2="1.0000000"
+ y2="0.50000000" />
+ <linearGradient
+ xlink:href="#linearGradient1528"
+ id="linearGradient1197"
+ x1="-3.2153100e-15"
+ y1="0.50000000"
+ x2="1.0000000"
+ y2="0.50000000" />
+ <linearGradient
+ xlink:href="#linearGradient1528"
+ id="linearGradient1199"
+ x1="2.6298408e-15"
+ y1="0.50000000"
+ x2="1.0000000"
+ y2="0.50000000" />
+ <linearGradient
+ xlink:href="#linearGradient1528"
+ id="linearGradient1201"
+ x1="3.3252481e-16"
+ y1="0.50000000"
+ x2="1.0000000"
+ y2="0.50000000" />
+ <linearGradient
+ xlink:href="#linearGradient1535"
+ id="linearGradient1447" />
+ <linearGradient
+ xlink:href="#linearGradient1528"
+ id="linearGradient1449"
+ x1="1.5254725e-16"
+ y1="0.50000000"
+ x2="1.0000000"
+ y2="0.50000000" />
+ <linearGradient
+ xlink:href="#linearGradient1535"
+ id="linearGradient1451" />
+ <linearGradient
+ xlink:href="#linearGradient1528"
+ id="linearGradient1454"
+ x1="-2.1412993e-17"
+ y1="0.50000000"
+ x2="1.0000000"
+ y2="0.50000000" />
+ <linearGradient
+ xlink:href="#linearGradient1535"
+ id="linearGradient1456" />
+ <linearGradient
+ xlink:href="#linearGradient1528"
+ id="linearGradient1458"
+ x1="3.5778672e-18"
+ y1="0.50000000"
+ x2="1.0000000"
+ y2="0.50000000" />
+ <linearGradient
+ xlink:href="#linearGradient1535"
+ id="linearGradient1460" />
+ <linearGradient
+ xlink:href="#linearGradient1528"
+ id="linearGradient1462"
+ x1="-2.7430315e-17"
+ y1="0.50000000"
+ x2="1.0000000"
+ y2="0.50000000" />
+ <linearGradient
+ xlink:href="#linearGradient1535"
+ id="linearGradient1473" />
+ <linearGradient
+ xlink:href="#linearGradient1528"
+ id="linearGradient1477"
+ x1="-3.1528599e-16"
+ y1="0.50000000"
+ x2="1.0000000"
+ y2="0.50000000" />
+ <linearGradient
+ xlink:href="#linearGradient1528"
+ id="linearGradient1479"
+ x1="4.2674198e-16"
+ y1="0.50000000"
+ x2="1.0000000"
+ y2="0.50000000" />
+ <linearGradient
+ xlink:href="#linearGradient1535"
+ id="linearGradient1481" />
+ <linearGradient
+ xlink:href="#linearGradient1535"
+ id="linearGradient1483" />
+ <linearGradient
+ xlink:href="#linearGradient1528"
+ id="linearGradient1485"
+ x1="6.1105634e-16"
+ y1="0.50000000"
+ x2="1.0000000"
+ y2="0.50000000" />
+ <linearGradient
+ xlink:href="#linearGradient1535"
+ id="linearGradient1487" />
+ <linearGradient
+ xlink:href="#linearGradient1528"
+ id="linearGradient1489"
+ x1="-3.9464959e-17"
+ y1="0.50000000"
+ x2="1.0000000"
+ y2="0.50000000" />
+ <linearGradient
+ xlink:href="#linearGradient1535"
+ id="linearGradient1503" />
+ <linearGradient
+ xlink:href="#linearGradient1528"
+ id="linearGradient1506"
+ x1="4.2121254e-17"
+ y1="0.50000000"
+ x2="1.0000000"
+ y2="0.50000000" />
+ <linearGradient
+ xlink:href="#linearGradient1535"
+ id="linearGradient1509" />
+ <linearGradient
+ xlink:href="#linearGradient1528"
+ id="linearGradient1511"
+ x1="1.0737938e-15"
+ y1="0.50000000"
+ x2="1.0000000"
+ y2="0.50000000" />
+ <linearGradient
+ xlink:href="#linearGradient1535"
+ id="linearGradient1523"
+ x1="-1.9434324e-17"
+ y1="0.50000000"
+ x2="1.0000000"
+ y2="0.50000000" />
+ <defs
+ id="defs1215">
+ <marker
+ id="ArrowEnd"
+ viewBox="0 0 10 10"
+ refX="0"
+ refY="5"
+ markerUnits="strokeWidth"
+ markerWidth="4"
+ markerHeight="3"
+ orient="auto">
+ <path
+ d="M 0 0 L 10 5 L 0 10 z"
+ id="path1217" />
+ </marker>
+ <marker
+ id="ArrowStart"
+ viewBox="0 0 10 10"
+ refX="10"
+ refY="5"
+ markerUnits="strokeWidth"
+ markerWidth="4"
+ markerHeight="3"
+ orient="auto">
+ <path
+ d="M 10 0 L 0 5 L 10 10 z"
+ id="path1219" />
+ </marker>
+ </defs>
+ </defs>
+ <path
+ style="fill:#000000;stroke:#261933;stroke-width:0.0000000;fill-opacity:0.30000001;"
+ d="M 4.7106983,22.843690 C 7.6318846,19.956735 7.1733586,20.085458 9.8330406,18.903380 C 11.162875,18.312416 11.659467,18.052135 12.971656,18.045274 C 14.283860,18.038263 16.137462,18.216059 17.439375,18.637879 C 20.043153,19.481518 18.908656,19.395901 20.386217,19.001973 C 21.863776,18.607898 21.653910,18.312416 21.752355,17.228782 C 21.850799,16.145209 21.949393,9.4467720 22.737395,7.9691683 C 23.525397,6.4915623 23.426954,6.0975313 24.411993,7.7721453 C 25.397033,9.4467570 25.791109,13.584046 26.382223,14.963130 C 26.973188,16.342231 27.170225,17.130278 27.465707,17.327286 C 27.761190,17.524309 27.564301,17.721317 29.041861,17.425805 C 30.519420,17.130278 32.095575,17.228782 33.376097,17.327286 C 34.656769,17.425805 35.247733,17.524309 35.838847,17.425805 C 36.429811,17.327286 36.528404,17.524309 36.922331,16.834751 C 37.316406,16.145209 38.399891,13.387022 38.498485,12.007936 C 38.596929,10.628835 38.301448,5.8020043 37.710483,4.7184303 C 37.119369,3.6348663 37.710483,3.1423274 38.991005,4.7184303 C 40.271527,6.2945543 41.436737,8.7572293 41.929257,10.037796 C 42.421777,11.318378 42.228189,11.063624 42.499902,12.935245 C 42.302865,14.806880 42.068650,16.306287 41.335891,17.260077 C 40.989682,18.103338 40.468565,18.965999 41.650643,18.903380 C 42.832721,18.509305 43.325241,17.721317 44.507319,16.933270 C 45.689397,16.145209 48.644666,14.569114 51.008822,14.273588 C 53.372979,13.978061 53.865499,14.175084 54.850538,14.569114 C 55.835579,14.963130 55.835579,14.470596 57.116251,15.455665 C 58.396773,16.440735 59.972925,17.524309 58.790849,19.001973 C 57.608771,20.479534 56.131209,20.184052 55.441652,20.972054 C 54.752094,21.760056 52.978902,22.351169 51.993863,22.548208 C 51.008822,22.745097 45.985028,22.942134 45.295471,22.745097 C 44.605912,22.548208 43.029759,21.267537 43.817762,23.139172 C 44.605912,25.010809 45.590952,29.542082 45.787991,31.315274 C 45.985028,33.088317 46.674586,38.900261 47.068513,41.461455 C 47.462588,44.022649 47.758071,46.288212 47.955108,48.947999 C 48.152147,51.607637 47.462588,51.903120 47.265551,53.085197 C 47.068513,53.770090 47.167106,53.774755 45.787991,54.759795 C 44.408875,55.744836 44.267174,55.025446 42.494132,55.616410 C 40.720941,56.207523 40.468565,56.040466 38.793968,56.926914 C 37.119369,57.813509 33.868617,59.192625 32.588095,58.108991 C 31.307424,57.025507 29.534381,54.562757 27.564301,52.888159 C 25.594071,51.213562 22.934434,47.371845 22.343319,41.067379 C 21.752355,34.762914 20.270470,28.133135 19.396850,26.060135 C 18.523231,23.987137 19.250825,23.704631 16.827005,25.109253 C 14.403201,26.514023 12.591228,26.586961 10.818111,26.586961 C 9.0449786,26.586961 6.2867916,25.404883 4.8092023,24.616733 C 3.3315993,23.828730 3.5286113,24.025768 4.7106983,22.843690 z "
+ id="path4368"
+ sodipodi:nodetypes="ccccccccccccccccccccccccccccccccccccccccccccc" />
+ <path
+ style="fill:#261933;stroke:#261933;stroke-width:0.0000000;"
+ d="M 2.4785706,21.314150 C 5.3997572,18.427195 4.9412304,18.555918 7.6009132,17.373840 C 8.9307470,16.782876 9.4273392,16.522595 10.739528,16.515734 C 12.051732,16.508723 13.905334,16.686519 15.207247,17.108339 C 17.811025,17.951978 16.676528,17.866361 18.154089,17.472433 C 19.631648,17.078358 19.421782,16.782876 19.520227,15.699242 C 19.618671,14.615669 19.717265,7.9172318 20.505267,6.4396275 C 21.293269,4.9620222 21.194826,4.5679910 22.179865,6.2426046 C 23.164905,7.9172172 23.558981,12.054506 24.150095,13.433590 C 24.741060,14.812691 24.938097,15.600738 25.233579,15.797746 C 25.529062,15.994769 25.332173,16.191777 26.809733,15.896265 C 28.287292,15.600738 29.863447,15.699242 31.143969,15.797746 C 32.424641,15.896265 33.015605,15.994769 33.606719,15.896265 C 34.197683,15.797746 34.296276,15.994769 34.690203,15.305211 C 35.084278,14.615669 36.167763,11.857482 36.266357,10.478396 C 36.364801,9.0992954 36.069320,4.2724639 35.478355,3.1888899 C 34.887241,2.1053255 35.478355,1.6127872 36.758877,3.1888899 C 38.039399,4.7650139 39.204609,7.2276890 39.697129,8.5082558 C 40.189649,9.7888382 39.996061,9.5340840 40.267774,11.405705 C 40.070737,13.277340 39.836522,14.776747 39.103763,15.730537 C 38.757554,16.573798 38.236437,17.436459 39.418515,17.373840 C 40.600593,16.979765 41.093113,16.191777 42.275191,15.403730 C 43.457269,14.615669 46.412538,13.039574 48.776694,12.744048 C 51.140851,12.448521 51.633371,12.645544 52.618410,13.039574 C 53.603451,13.433590 53.603451,12.941056 54.884123,13.926125 C 56.164645,14.911195 57.740797,15.994769 56.558721,17.472433 C 55.376643,18.949994 53.899081,18.654512 53.209524,19.442514 C 52.519966,20.230516 50.746774,20.821629 49.761735,21.018668 C 48.776694,21.215557 43.752900,21.412594 43.063343,21.215557 C 42.373784,21.018668 40.797631,19.737997 41.585634,21.609632 C 42.373784,23.481269 43.358824,28.012542 43.555863,29.785734 C 43.752900,31.558777 44.442458,37.370721 44.836385,39.931915 C 45.230460,42.493109 45.525943,44.758672 45.722980,47.418459 C 45.920019,50.078097 45.230460,50.373580 45.033423,51.555657 C 44.836385,52.240550 44.934978,52.245215 43.555863,53.230255 C 42.176747,54.215296 42.035046,53.495906 40.262004,54.086870 C 38.488813,54.677983 38.236437,54.510926 36.561840,55.397374 C 34.887241,56.283969 31.636489,57.663085 30.355967,56.579451 C 29.075296,55.495967 27.302253,53.033217 25.332173,51.358619 C 23.361943,49.684022 20.702306,45.842305 20.111191,39.537839 C 19.520227,33.233374 18.038342,26.603595 17.164722,24.530595 C 16.291103,22.457597 17.018697,22.175091 14.594877,23.579713 C 12.171073,24.984483 10.359100,25.057421 8.5859832,25.057421 C 6.8128511,25.057421 4.0546643,23.875343 2.5770746,23.087193 C 1.0994716,22.299190 1.2964836,22.496228 2.4785706,21.314150 z "
+ id="path1221"
+ sodipodi:nodetypes="ccccccccccccccccccccccccccccccccccccccccccccc" />
+ <path
+ style="fill:url(#linearGradient1532);stroke:#261933;stroke-width:0.0000000;"
+ d="M 44.761526,15.526345 C 46.250344,15.148311 46.561941,15.113941 47.185131,14.804642 C 47.808322,14.495343 48.293044,14.529711 49.678048,14.564079 C 51.063053,14.598449 51.616933,14.770274 52.240124,15.045220 C 52.863466,15.320150 53.140407,15.388872 52.240124,15.526345 C 51.339993,15.663817 49.089511,15.870011 47.669853,15.973101 C 46.250344,16.076205 45.073122,16.213709 44.380620,16.557390 C 43.688117,16.901071 43.099582,16.935322 43.480487,16.557390 C 43.861243,16.179310 44.207494,15.698185 44.761526,15.526345 z "
+ id="path1224"
+ sodipodi:nodetypes="ccccccccc" />
+ <path
+ style="fill:url(#linearGradient1527);stroke:#261933;stroke-width:0.0000000;"
+ d="M 47.458966,17.382197 C 49.294035,17.244603 50.367292,17.141560 51.232921,16.901071 C 52.098546,16.660434 51.925421,16.832275 52.825553,16.797879 C 53.725836,16.763479 53.829650,16.866674 53.691182,17.107161 C 53.552710,17.347798 52.894865,17.931967 52.237017,18.138207 C 51.579172,18.344447 51.094449,18.310048 50.263477,18.894216 C 49.432505,19.478537 48.151468,19.719024 46.731809,19.684626 C 45.312300,19.650377 44.308203,19.890865 43.581044,19.547183 C 42.854038,19.203501 42.611603,18.997413 43.200290,18.447491 C 43.788826,17.897571 44.239043,17.931967 45.312300,17.725879 C 46.385559,17.519639 46.870279,17.416446 47.458966,17.382197 z "
+ id="path1225"
+ sodipodi:nodetypes="ccccccccccc" />
+ <path
+ style="fill:url(#linearGradient1539);stroke:#261933;stroke-width:0.0000000;"
+ d="M 20.736032,13.830826 C 20.772962,11.931359 20.995356,11.200789 21.217749,10.470220 C 21.440142,9.7396501 21.403050,9.8857720 21.699466,10.470220 C 21.995883,11.054669 22.403577,12.588869 22.551786,13.100257 C 22.699994,13.611661 23.107687,14.634455 23.292989,15.036270 C 23.478289,15.438068 23.700683,15.839931 22.996572,15.912896 C 22.292462,15.986019 21.995883,16.095546 21.514165,16.095546 C 21.032448,16.095546 20.624755,15.766808 20.661847,15.109330 C 20.698939,14.451805 20.698939,14.013476 20.736032,13.830826 z "
+ id="path1226"
+ sodipodi:nodetypes="ccccccccc" />
+ <path
+ style="fill:url(#linearGradient1534);stroke:#261933;stroke-width:0.0000000;"
+ d="M 37.459144,11.412117 C 37.559057,10.526575 37.499613,9.7722168 37.499613,9.4442368 C 37.499613,9.1162710 37.504547,8.7157005 37.504547,8.4861188 C 37.504547,8.2565371 37.525752,7.8371360 37.525752,7.5091559 C 37.525752,7.1811901 37.302894,5.3610879 37.668952,6.4106269 C 38.035011,7.4601517 38.700316,8.8158688 38.822815,10.076022 C 39.055656,11.059948 38.737737,12.105753 38.737737,12.532117 C 38.737737,12.958495 38.490711,13.642364 38.191261,14.527906 C 37.891811,15.413462 37.804948,16.263251 37.305818,16.296050 C 36.806831,16.328849 36.394418,16.069422 35.928591,16.069422 C 35.462764,16.069422 35.496069,16.003823 35.828823,15.577445 C 36.161577,15.151066 37.226303,13.183215 37.459144,11.412117 z "
+ id="path1227"
+ sodipodi:nodetypes="cccccccccccc" />
+ <path
+ style="fill:url(#linearGradient1522);stroke:#261933;stroke-width:0.0000000;"
+ d="M 22.084966,33.556261 C 21.731894,30.873040 21.661372,29.390231 21.096427,27.624873 C 20.531635,25.859669 18.978151,21.693639 18.483881,20.563748 C 17.989612,19.434011 18.272007,19.151615 19.472420,18.869066 C 20.672833,18.586670 25.191935,17.174383 27.875156,17.315735 C 30.558377,17.456933 32.535455,16.468394 35.359875,18.021725 C 38.184294,19.575209 39.102311,18.798544 39.808300,21.764161 C 40.514444,24.729778 41.361785,31.649705 41.785532,33.838657 C 42.209126,36.027609 43.127143,43.583003 43.409539,44.571542 C 43.691935,45.560081 43.974484,45.842477 42.067928,45.771955 C 40.161372,45.701279 37.690024,45.560081 34.724407,46.972368 C 31.758790,48.384501 29.287289,50.220381 28.581299,51.067722 C 27.875156,51.915063 27.875156,51.491469 26.957139,50.714650 C 26.039276,49.937985 24.203396,47.254764 23.779648,44.924614 C 23.356054,42.594464 22.367515,37.934165 22.296840,36.733752 C 22.226164,35.533339 22.084966,34.474124 22.084966,33.556261 z "
+ id="path1228"
+ sodipodi:nodetypes="cccccccccccccccc" />
+ <path
+ style="fill:#261933;stroke:#261933;stroke-width:0.0000000;"
+ d="M 25.385484,29.639542 C 26.979950,28.987311 27.487258,28.914928 28.212030,29.567160 C 28.936802,30.219391 29.661573,31.324684 28.936802,32.049298 C 28.212030,32.774069 27.849644,32.755934 27.052490,33.335783 C 26.255178,33.915632 25.385484,33.625629 24.878175,33.335783 C 24.370866,33.045938 24.225944,32.611012 24.081021,31.958780 C 23.936098,31.306549 24.443407,30.291932 25.385484,29.639542 z "
+ id="path1229"
+ sodipodi:nodetypes="ccccccc" />
+ <path
+ style="fill:#261933;stroke:#261933;stroke-width:0.0000000;"
+ d="M 35.728856,25.073775 C 37.029493,25.120068 37.470789,25.445328 37.470789,26.374412 C 37.470789,27.303295 36.402220,27.884074 35.751901,27.884074 C 35.101582,27.884074 34.404972,27.326543 34.218993,26.583436 C 34.033217,25.840128 34.311982,25.050527 35.728856,25.073775 z "
+ id="path1230"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ style="fill:url(#linearGradient1534);stroke:none;stroke-width:0.62500000;fill-opacity:1.0000000;"
+ d="M 14.139202,20.835913 C 14.742777,20.863369 14.697703,20.940553 14.697702,21.297281 C 14.697702,21.653891 14.769310,21.566558 14.357745,21.758652 C 13.946239,21.950747 11.738935,22.725891 10.861042,22.972785 C 9.9831498,23.219676 9.2233469,23.479544 8.8941471,23.507003 C 8.5649360,23.534342 7.7827156,23.345158 6.4658823,22.851372 C 5.7251673,22.659276 5.3807684,22.524000 5.7374025,22.414285 C 6.0940479,22.304567 6.1379707,22.245195 6.8786883,22.025762 C 7.6194033,21.806326 10.341096,21.267655 11.273848,21.103020 C 12.206611,20.938385 13.535649,20.808452 14.139202,20.835913 z "
+ id="path1009" />
+ <path
+ style="fill:url(#linearGradient1532);stroke:none;stroke-width:0.62500000;fill-opacity:1.0000000;"
+ d="M 9.8126320,18.990430 C 12.192522,19.092081 12.659756,19.405194 13.472245,19.709839 C 14.284749,20.014487 13.674620,20.093002 12.658998,19.991348 C 11.643375,19.889852 8.3105098,19.960070 7.4980037,19.960070 C 6.6855188,19.960070 6.6619605,19.732978 7.3728917,19.428331 C 8.0838196,19.123532 8.8985682,18.990430 9.8126320,18.990430 z "
+ id="path1008" />
+</svg>
diff --git a/activity/activity.info b/activity/activity.info
new file mode 100644
index 0000000..172e438
--- /dev/null
+++ b/activity/activity.info
@@ -0,0 +1,7 @@
+[Activity]
+name = gtktest
+service_name = org.laptop.gtktest
+class = activity.gtktestActivity
+icon = activity-gtktest
+activity_version = 1
+show_launcher = yes \ No newline at end of file
diff --git a/button.py b/button.py
new file mode 100644
index 0000000..66cf80b
--- /dev/null
+++ b/button.py
@@ -0,0 +1,71 @@
+import gobject
+import gtk
+from gettext import gettext as _
+
+from sugar.graphics.palette import Palette
+from sugar.graphics.tray import TrayButton
+import constants
+import utils
+
+class RecdButton(TrayButton):
+ __gsignals__ = {
+ 'remove-requested': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+ 'copy-clipboard-requested': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+ }
+
+ def __init__(self, recd):
+ super(RecdButton, self).__init__()
+ self._recd = recd
+
+ self.set_icon_widget(self.get_image())
+ self._copy_menu_item_handler = None
+
+ palette = Palette(recd.title)
+ self.set_palette(palette)
+
+ self._rem_menu_item = gtk.MenuItem(_('Remove'))
+ self._rem_menu_item_handler = self._rem_menu_item.connect('activate', self._remove_clicked)
+ palette.menu.append(self._rem_menu_item)
+ self._rem_menu_item.show()
+
+ self._add_copy_menu_item()
+
+ def _add_copy_menu_item( self ):
+ if self._recd.buddy and not self._recd.downloadedFromBuddy:
+ return
+
+ self._copy_menu_item = gtk.MenuItem(_('Copy to clipboard'))
+ self._copy_menu_item_handler = self._copy_menu_item.connect('activate', self._copy_clipboard_clicked)
+ self.get_palette().menu.append(self._copy_menu_item)
+ self._copy_menu_item.show()
+
+ def get_recd(self):
+ return self._recd
+
+ def get_image(self):
+ img = gtk.Image()
+ ipb = self._recd.getThumbPixbuf()
+ if self._recd.type == constants.TYPE_PHOTO:
+ path = 'object-photo.svg'
+ elif self._recd.type == constants.TYPE_VIDEO:
+ path = 'object-video.svg'
+ elif self._recd.type == constants.TYPE_AUDIO:
+ path = 'object-audio.svg'
+
+ pixbuf = utils.load_colored_svg(path, self._recd.colorStroke, self._recd.colorFill)
+ if ipb:
+ ipb.composite(pixbuf, 8, 8, ipb.get_width(), ipb.get_height(), 8, 8, 1, 1, gtk.gdk.INTERP_BILINEAR, 255)
+ img.set_from_pixbuf(pixbuf)
+ img.show()
+ return img
+
+ def cleanup(self):
+ self._rem_menu_item.disconnect(self._rem_menu_item_handler)
+ if self._copy_menu_item_handler != None:
+ self._copy_menu_item.disconnect(self._copy_menu_item_handler)
+
+ def _remove_clicked(self, widget):
+ self.emit('remove-requested')
+
+ def _copy_clipboard_clicked(self, widget):
+ self.emit('copy-clipboard-requested')
diff --git a/button.pyc b/button.pyc
new file mode 100644
index 0000000..e22f532
--- /dev/null
+++ b/button.pyc
Binary files differ
diff --git a/collab.py b/collab.py
new file mode 100644
index 0000000..c7ddca8
--- /dev/null
+++ b/collab.py
@@ -0,0 +1,339 @@
+import logging
+import xml.dom.minidom
+import os
+
+import gobject
+import telepathy
+import telepathy.client
+
+from sugar.presence import presenceservice
+from sugar.presence.tubeconn import TubeConnection
+from sugar import util
+
+import utils
+import serialize
+import constants
+from instance import Instance
+#from recordtube import RecordTube
+from recorded import Recorded
+
+logger = logging.getLogger('collab')
+
+class RecordCollab(object):
+ def __init__(self, activity_obj, model):
+ self.activity = activity_obj
+ self.model = model
+ self._tube = None
+ self._collab_timeout = 10000
+
+ def set_activity_shared(self):
+ self._setup()
+ self._tubes_channel.OfferDBusTube(constants.DBUS_SERVICE, {})
+
+ def share_recd(self, recd):
+ if not self._tube:
+ return
+ xmlstr = serialize.getRecdXmlMeshString(recd)
+ self._tube.notifyBudsOfNewRecd(Instance.keyHashPrintable, xmlstr)
+
+ def joined(self):
+ if not self.activity.get_shared_activity():
+ return
+ self._setup()
+ self._tubes_channel.ListTubes(reply_handler=self._list_tubes_reply_cb, error_handler=self._list_tubes_error_cb)
+
+ def request_download(self, recd):
+ if recd.meshDownloading:
+ logger.debug("meshInitRoundRobin: we are in midst of downloading this file...")
+ return
+
+ # start with who took the photo
+ recd.triedMeshBuddies = []
+ recd.triedMeshBuddies.append(Instance.keyHashPrintable)
+ self._req_recd_from_buddy(recd, recd.recorderHash, recd.recorderName)
+
+ def _list_tubes_reply_cb(self, tubes):
+ for tube_info in tubes:
+ self._new_tube_cb(*tube_info)
+
+ @staticmethod
+ def _list_tubes_error_cb(e):
+ logger.error('ListTubes() failed: %s', e)
+
+ def _setup(self):
+ # sets up the tubes...
+ if not self.activity.get_shared_activity():
+ logger.error('_setup: Failed to share or join activity')
+ return
+
+ pservice = presenceservice.get_instance()
+ try:
+ name, path = pservice.get_preferred_connection()
+ self._connection = telepathy.client.Connection(name, path)
+ except:
+ logger.error('_setup: Failed to get_preferred_connection')
+
+ # Work out what our room is called and whether we have Tubes already
+ bus_name, conn_path, channel_paths = self.activity._shared_activity.get_channels()
+ 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._connection.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 not room:
+ logger.error("Presence service didn't create a room")
+ return
+ if not text_chan:
+ 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 not tubes_chan:
+ logger.debug("Didn't find our Tubes channel, requesting one...")
+ tubes_chan = self._connection.request_channel(telepathy.CHANNEL_TYPE_TUBES, telepathy.HANDLE_TYPE_ROOM, room, True)
+
+ self._tubes_channel = tubes_chan[telepathy.CHANNEL_TYPE_TUBES]
+ self._text_channel = text_chan[telepathy.CHANNEL_INTERFACE_GROUP]
+
+ self._tubes_channel.connect_to_signal('NewTube', self._new_tube_cb)
+
+ # 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 or service != constants.DBUS_SERVICE:
+ # return
+ #
+ # if state == telepathy.TUBE_STATE_LOCAL_PENDING:
+ # self._tubes_channel.AcceptDBusTube(id)
+ # tube_connection = TubeConnection(self._connection, self._tubes_channel, id, group_iface=self._text_channel)
+ # self._tube = RecordTube(tube_connection)
+ # self._tube.connect("new-recd", self._new_recd_cb)
+ # self._tube.connect("recd-request", self._recd_request_cb)
+ # self._tube.connect("recd-bits-arrived", self._recd_bits_arrived_cb)
+ # self._tube.connect("recd-unavailable", self._recd_unavailable_cb)
+
+ def _new_recd_cb(self, remote_object, recorder, xmlstr):
+ logger.debug('new_recd_cb')
+ dom = None
+ try:
+ dom = xml.dom.minidom.parseString(xmlstr)
+ except:
+ logger.error('Unable to parse mesh xml')
+ if not dom:
+ return
+
+ recd = Recorded()
+ recd = serialize.fillRecdFromNode(recd, dom.documentElement)
+ if not recd:
+ logger.debug('_newRecdCb: recd is None. Unable to parse XML')
+ return
+
+ logger.debug('_newRecdCb: adding new recd thumb')
+ recd.buddy = True
+ recd.downloadedFromBuddy = False
+ self.model.add_recd(recd)
+
+ def _req_recd_from_buddy(self, recd, sender, nick):
+ recd.triedMeshBuddies.append(sender)
+ recd.meshDownloadingFrom = sender
+ recd.meshDownloadingFromNick = nick
+ recd.meshDownloadingProgress = False
+ recd.meshDownloading = True
+ recd.meshDownlodingPercent = 0.0
+ self.activity.update_download_progress(recd)
+ recd.meshReqCallbackId = gobject.timeout_add(self._collab_timeout, self._check_recd_request, recd)
+ self._tube.requestRecdBits(Instance.keyHashPrintable, sender, recd.mediaMd5)
+
+ def _next_round_robin_buddy(self, recd):
+ logger.debug('meshNextRoundRobinBuddy')
+ if recd.meshReqCallbackId:
+ gobject.source_remove(recd.meshReqCallbackId)
+ recd.meshReqCallbackId = 0
+
+ # delete any stub of a partially downloaded file
+ path = recd.getMediaFilepath()
+ if path and os.path.exists(path):
+ os.remove(path)
+
+ good_buddy_obj = None
+ buds = self.activity._shared_activity.get_joined_buddies()
+ for buddy_obj in buds:
+ buddy = util.sha_data(buddy_obj.props.key)
+ buddy = util.printable_hash(buddy)
+ if recd.triedMeshBuddies.count(buddy) > 0:
+ logger.debug('mnrrb: weve already tried bud ' + buddy_obj.props.nick)
+ else:
+ logger.debug('mnrrb: ask next buddy: ' + buddy_obj.props.nick)
+ good_buddy_obj = buddy_obj
+ break
+
+ if good_buddy_obj:
+ buddy = util.sha_data(good_buddy_obj.props.key)
+ buddy = util.printable_hash(buddy)
+ self._req_recd_from_buddy(recd, buddy, good_buddy_obj.props.nick)
+ else:
+ logger.debug('weve tried all buddies here, and no one has this recd')
+ recd.meshDownloading = False
+ recd.triedMeshBuddies = []
+ recd.triedMeshBuddies.append(Instance.keyHashPrintable)
+ self.activity.update_download_progress(recd)
+
+ def _recd_request_cb(self, remote_object, remote_person, md5sum):
+ #if we are here, it is because someone has been told we have what they want.
+ #we need to send them that thing, whatever that thing is
+ recd = self.model.get_recd_by_md5(md5sum)
+ if not recd:
+ logger.debug('_recdRequestCb: we dont have the recd they asked for')
+ self._tube.unavailableRecd(md5sum, Instance.keyHashPrintable, remote_person)
+ return
+
+ if recd.deleted:
+ logger.debug('_recdRequestCb: we have the recd, but it has been deleted, so we wont share')
+ self._tube.unavailableRecd(md5sum, Instance.keyHashPrintable, remote_person)
+ return
+
+ if recd.buddy and not recd.downloadedFromBuddy:
+ logger.debug('_recdRequestCb: we have an incomplete recd, so we wont share')
+ self._tube.unavailableRecd(md5sum, Instance.keyHashPrintable, remote_person)
+ return
+
+ recd.meshUploading = True
+ path = recd.getMediaFilepath()
+
+ if recd.type == constants.TYPE_AUDIO:
+ audioImgFilepath = recd.getAudioImageFilepath()
+
+ dest_path = os.path.join(Instance.instancePath, "audioBundle")
+ dest_path = utils.getUniqueFilepath(dest_path, 0)
+ cmd = "cat " + path + " " + audioImgFilepath + " > " + dest_path
+ logger.debug(cmd)
+ os.system(cmd)
+ path = dest_path
+
+ self._tube.broadcastRecd(recd.mediaMd5, path, remote_person)
+ recd.meshUploading = False
+ #if you were deleted while uploading, now throw away those bits now
+ if recd.deleted:
+ recd.doDeleteRecorded(recd)
+
+ def _check_recd_request(self, recd):
+ #todo: add category for "not active activity, so go ahead and delete"
+
+ if recd.downloadedFromBuddy:
+ logger.debug('_meshCheckOnRecdRequest: recdRequesting.downloadedFromBuddy')
+ if recd.meshReqCallbackId:
+ gobject.source_remove(recd.meshReqCallbackId)
+ recd.meshReqCallbackId = 0
+ return False
+ if recd.deleted:
+ logger.debug('_meshCheckOnRecdRequest: recdRequesting.deleted')
+ if recd.meshReqCallbackId:
+ gobject.source_remove(recd.meshReqCallbackId)
+ recd.meshReqCallbackId = 0
+ return False
+ if recd.meshDownloadingProgress:
+ logger.debug('_meshCheckOnRecdRequest: recdRequesting.meshDownloadingProgress')
+ #we've received some bits since last we checked, so keep waiting... they'll all get here eventually!
+ recd.meshDownloadingProgress = False
+ return True
+ else:
+ logger.debug('_meshCheckOnRecdRequest: ! recdRequesting.meshDownloadingProgress')
+ #that buddy we asked info from isn't responding; next buddy!
+ #self.meshNextRoundRobinBuddy( recdRequesting )
+ gobject.idle_add(self._next_round_robin_buddy, recd)
+ return False
+
+ def _recd_bits_arrived_cb(self, remote_object, md5sum, part, num_parts, bytes, sender):
+ recd = self.model.get_recd_by_md5(md5sum)
+ if not recd:
+ logger.debug('_recdBitsArrivedCb: thx 4 yr bits, but we dont even have that photo')
+ return
+ if recd.deleted:
+ logger.debug('_recdBitsArrivedCb: thx 4 yr bits, but we deleted that photo')
+ return
+ if recd.downloadedFromBuddy:
+ logger.debug('_recdBitsArrivedCb: weve already downloadedFromBuddy')
+ return
+ if not recd.buddy:
+ logger.debug('_recdBitsArrivedCb: uh, we took this photo, so dont need your bits')
+ return
+ if recd.meshDownloadingFrom != sender:
+ logger.debug('_recdBitsArrivedCb: wrong bits ' + str(sender) + ", exp:" + str(recd.meshDownloadingFrom))
+ return
+
+ #update that we've heard back about this, reset the timeout
+ gobject.source_remove(recd.meshReqCallbackId)
+ recd.meshReqCallbackId = gobject.timeout_add(self._collab_timeout, self._check_recd_request, recd)
+
+ #update the progress bar
+ recd.meshDownlodingPercent = (part+0.0)/(num_parts+0.0)
+ recd.meshDownloadingProgress = True
+ self.activity.update_download_progress(recd)
+ open(recd.getMediaFilepath(), 'a+').write(bytes)
+
+ if part > num_parts:
+ logger.error('More parts than required have arrived')
+ return
+ if part != num_parts:
+ return
+
+ logger.debug('Finished receiving %s' % recd.title)
+ gobject.source_remove(recd.meshReqCallbackId)
+ recd.meshReqCallbackId = 0
+ recd.meshDownloading = False
+ recd.meshDownlodingPercent = 1.0
+ recd.downloadedFromBuddy = True
+ if recd.type == constants.TYPE_AUDIO:
+ path = recd.getMediaFilepath()
+ bundle_path = os.path.join(Instance.instancePath, "audioBundle")
+ bundle_path = utils.getUniqueFilepath(bundle_path, 0)
+
+ cmd = "split -a 1 -b " + str(recd.mediaBytes) + " " + path + " " + bundle_path
+ logger.debug(cmd)
+ os.system(cmd)
+
+ bundle_name = os.path.basename(bundle_path)
+ media_filename = bundle_name + "a"
+ media_path = os.path.join(Instance.instancePath, media_filename)
+ media_path_ext = os.path.join(Instance.instancePath, media_filename+".ogg")
+ os.rename(media_path, media_path_ext)
+ audio_image_name = bundle_name + "b"
+ audio_image_path = os.path.join(Instance.instancePath, audio_image_name)
+ audio_image_path_ext = os.path.join(Instance.instancePath, audio_image_name+".png")
+ os.rename(audio_image_path, audio_image_path_ext)
+
+ recd.mediaFilename = os.path.basename(media_path_ext)
+ recd.audioImageFilename = os.path.basename(audio_image_path_ext)
+
+ self.activity.remote_recd_available(recd)
+
+ def _recd_unavailable_cb(self, remote_object, md5sum, sender):
+ logger.debug('_recdUnavailableCb: sux, we want to see that photo')
+ recd = self.model.get_recd_by_md5(md5sum)
+ if not recd:
+ logger.debug('_recdUnavailableCb: actually, we dont even know about that one..')
+ return
+ if recd.deleted:
+ logger.debug('_recdUnavailableCb: actually, since we asked, we deleted.')
+ return
+ if not recd.buddy:
+ logger.debug('_recdUnavailableCb: uh, odd, we took that photo and have it already.')
+ return
+ if recd.downloadedFromBuddy:
+ logger.debug('_recdUnavailableCb: we already downloaded it... you might have been slow responding.')
+ return
+ if recd.meshDownloadingFrom != sender:
+ logger.debug('_recdUnavailableCb: we arent asking you for a copy now. slow response, pbly.')
+ return
+
diff --git a/collab.pyc b/collab.pyc
new file mode 100644
index 0000000..361017c
--- /dev/null
+++ b/collab.pyc
Binary files differ
diff --git a/constants.py b/constants.py
new file mode 100644
index 0000000..9b0ed4a
--- /dev/null
+++ b/constants.py
@@ -0,0 +1,46 @@
+# -*- coding: UTF-8 -*-
+import os
+from gettext import gettext as _
+
+from sugar.activity import activity
+
+MODE_PHOTO = 0
+MODE_VIDEO = 1
+MODE_AUDIO = 2
+TYPE_PHOTO = MODE_PHOTO
+TYPE_VIDEO = MODE_VIDEO
+TYPE_AUDIO = MODE_AUDIO
+
+STATE_READY = 0
+STATE_RECORDING = 1
+STATE_PROCESSING = 2
+STATE_DOWNLOADING = 3
+
+MEDIA_INFO = {}
+MEDIA_INFO[TYPE_PHOTO] = {
+ 'name' : 'photo',
+ 'mime' : 'image/jpeg',
+ 'ext' : 'jpg',
+ 'istr' : _('Photo')
+}
+
+MEDIA_INFO[TYPE_VIDEO] = {
+ 'name' : 'video',
+ 'mime' : 'video/ogg',
+ 'ext' : 'ogg',
+ 'istr' : _('Video')
+}
+
+MEDIA_INFO[TYPE_AUDIO] = {
+ 'name' : 'audio',
+ 'mime' :'audio/ogg',
+ 'ext' : 'ogg',
+ 'istr' : _('Audio')
+}
+
+DBUS_SERVICE = "org.laptop.Record"
+DBUS_IFACE = DBUS_SERVICE
+DBUS_PATH = "/org/laptop/Record"
+
+GFX_PATH = os.path.join(activity.get_bundle_path(), "gfx")
+
diff --git a/constants.pyc b/constants.pyc
new file mode 100644
index 0000000..0f11f05
--- /dev/null
+++ b/constants.pyc
Binary files differ
diff --git a/gfx/corner-info.svg b/gfx/corner-info.svg
new file mode 100644
index 0000000..9a22582
--- /dev/null
+++ b/gfx/corner-info.svg
@@ -0,0 +1,15 @@
+<?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 "#010101">
+ <!ENTITY fill_color "#CDCCCC">
+]><svg enable-background="new 0 0 75 75" height="75px" version="1.1" viewBox="0 0 75 75" width="75px" x="0px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" y="0px"><g display="block" id="corner-info">
+ <g display="inline">
+ <rect height="75" width="75"/>
+ <g>
+ <polygon fill="&fill_color;" points="0,0 75,0 0,75 "/>
+ </g>
+ <g>
+ <path d="M22.34,35.03h2.534l-0.383,2.017h-8.436l3.051-15.704h-2.518l0.384-2.018h8.419L22.34,35.03z M19.923,14.308 c0.177-0.9,0.636-1.659,1.375-2.275c0.739-0.617,1.559-0.925,2.459-0.925c0.877,0,1.567,0.311,2.067,0.934 c0.367,0.456,0.55,0.99,0.55,1.601c0,0.21-0.022,0.433-0.066,0.667c-0.167,0.878-0.62,1.625-1.359,2.243 c-0.74,0.617-1.548,0.925-2.426,0.925c-0.9,0-1.6-0.306-2.101-0.917c-0.377-0.456-0.567-0.99-0.567-1.601 C19.856,14.747,19.879,14.53,19.923,14.308z"/>
+ </g>
+ <polyline fill="none" points="1,75 1,1 75,1 " stroke="#808080" stroke-width="2"/>
+ </g>
+</g></svg> \ No newline at end of file
diff --git a/gfx/max-enlarge.svg b/gfx/max-enlarge.svg
new file mode 100644
index 0000000..71ce290
--- /dev/null
+++ b/gfx/max-enlarge.svg
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 13.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ width="48.5px" height="48.5px" viewBox="0 0 48.5 48.5" enable-background="new 0 0 48.5 48.5" xml:space="preserve">
+<g>
+ <rect x="1.75" y="1.75" fill="#000000" stroke="#FFFFFF" stroke-width="3.5" width="45" height="45"/>
+ <g>
+
+ <line fill="none" stroke="#FFFFFF" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" x1="20.449" y1="28.048" x2="9.449" y2="39.049"/>
+ <polyline fill="none" stroke="#FFFFFF" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" points="9.386,28.048
+ 20.449,28.048 20.449,39.112 "/>
+ </g>
+ <g>
+
+ <line fill="none" stroke="#FFFFFF" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" x1="27.447" y1="21.049" x2="39.447" y2="9.048"/>
+ <polyline fill="none" stroke="#FFFFFF" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" points="38.51,21.049
+ 27.447,21.049 27.447,9.984 "/>
+ </g>
+</g>
+</svg>
diff --git a/gfx/max-reduce.svg b/gfx/max-reduce.svg
new file mode 100644
index 0000000..54f0be1
--- /dev/null
+++ b/gfx/max-reduce.svg
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 13.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ width="48.5px" height="48.5px" viewBox="0 0 48.5 48.5" enable-background="new 0 0 48.5 48.5" xml:space="preserve">
+
+<g>
+ <rect x="1.75" y="1.75" fill="#000000" stroke="#FFFFFF" stroke-width="3.5" width="45" height="45"/>
+ <g>
+
+ <line fill="none" stroke="#FFFFFF" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" x1="9.45" y1="39.051" x2="20.45" y2="28.05"/>
+ <polyline fill="none" stroke="#FFFFFF" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" points="
+ 20.513,39.051 9.45,39.051 9.45,27.986 "/>
+ </g>
+ <g>
+
+ <line fill="none" stroke="#FFFFFF" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" x1="39.448" y1="9.049" x2="27.448" y2="21.05"/>
+ <polyline fill="none" stroke="#FFFFFF" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" points="28.386,9.049
+ 39.448,9.049 39.448,20.114 "/>
+ </g>
+</g>
+</svg>
diff --git a/gfx/media-circle.png b/gfx/media-circle.png
new file mode 100644
index 0000000..60c7d92
--- /dev/null
+++ b/gfx/media-circle.png
Binary files differ
diff --git a/gfx/media-insensitive.png b/gfx/media-insensitive.png
new file mode 100644
index 0000000..3e90780
--- /dev/null
+++ b/gfx/media-insensitive.png
Binary files differ
diff --git a/gfx/media-pause.png b/gfx/media-pause.png
new file mode 100644
index 0000000..4308186
--- /dev/null
+++ b/gfx/media-pause.png
Binary files differ
diff --git a/gfx/media-play.png b/gfx/media-play.png
new file mode 100644
index 0000000..9a99e60
--- /dev/null
+++ b/gfx/media-play.png
Binary files differ
diff --git a/gfx/media-record-red.png b/gfx/media-record-red.png
new file mode 100644
index 0000000..289b54a
--- /dev/null
+++ b/gfx/media-record-red.png
Binary files differ
diff --git a/gfx/media-record.png b/gfx/media-record.png
new file mode 100644
index 0000000..41be533
--- /dev/null
+++ b/gfx/media-record.png
Binary files differ
diff --git a/gfx/object-audio.svg b/gfx/object-audio.svg
new file mode 100644
index 0000000..723a362
--- /dev/null
+++ b/gfx/object-audio.svg
@@ -0,0 +1,10 @@
+<?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 "#010101">
+ <!ENTITY fill_color "#CCCCCC">
+]><svg enable-background="new 0 0 123.5 123.5" height="123.5px" version="1.1" viewBox="0 0 123.5 123.5" width="123.5px" x="0px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" y="0px"><g display="block" id="object-audio_1_">
+ <rect fill="&fill_color;" height="120" stroke="&stroke_color;" stroke-width="3.5" width="120" x="1.75" y="1.75"/>
+ <g>
+ <path d=" M13.625,104.216c1.186-0.999,2.513-2.458,4.421-2.242c2.045,0.408,3.177,2.035,4.639,3.133c1.025,0.945,2.734,1.868,4.261,1.1 c2.358-1.07,3.49-3.422,6.083-4.194c1.918-0.391,3.319,1.063,4.505,2.062c1.197,1.034,2.452,2.42,4.365,2.435 c2.362-0.416,3.584-2.332,5.291-3.545c1.016-0.854,2.694-1.43,4.011-0.651c2.224,1.116,3.369,7.95,5.89,9.341 c1.996,0.771,4.659-7.438,4.659-7.438s2.514-7.895,4.422-7.497c2.045,0.755,3.177,7.29,4.639,8.388 c1.025,0.945,2.733,1.868,4.262,1.1c2.356-1.07,3.488-3.422,6.082-4.194c1.918-0.391,3.319,1.063,4.507,2.062 c1.196,1.034,2.452,2.42,4.363,2.435c2.362-0.416,3.585-2.332,5.291-3.545c1.016-0.854,2.694-1.43,4.012-0.651 c2.224,1.116,3.369,3.354,5.89,4.107c1.995,0.418,3.442-1.188,4.658-2.204" fill="none" id="path152_2_" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.25"/>
+ </g>
+ <rect fill="#EEEEEE" height="81" width="108" x="7.75" y="7.75"/>
+</g></svg> \ No newline at end of file
diff --git a/gfx/object-photo.svg b/gfx/object-photo.svg
new file mode 100644
index 0000000..4de3f7a
--- /dev/null
+++ b/gfx/object-photo.svg
@@ -0,0 +1,8 @@
+<?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 "#010101">
+ <!ENTITY fill_color "#CCCCCC">
+]><svg enable-background="new 0 0 123.5 123.5" height="123.5px" version="1.1" viewBox="0 0 123.5 123.5" width="123.5px" x="0px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" y="0px"><g display="block" id="object-photo">
+ <rect fill="&fill_color;" height="120" stroke="&stroke_color;" stroke-width="3.5" width="120" x="1.75" y="1.75"/>
+ <circle cx="61.75" cy="104.25" fill="#FFFFFF" r="5"/>
+ <rect fill="#EEEEEE" height="81" width="108" x="7.75" y="7.75"/>
+</g></svg> \ No newline at end of file
diff --git a/gfx/object-video.svg b/gfx/object-video.svg
new file mode 100644
index 0000000..5b536df
--- /dev/null
+++ b/gfx/object-video.svg
@@ -0,0 +1,14 @@
+<?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 "#010101">
+ <!ENTITY fill_color "#CCCCCC">
+]><svg enable-background="new 0 0 123.5 123.5" height="123.5px" version="1.1" viewBox="0 0 123.5 123.5" width="123.5px" x="0px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" y="0px"><g display="block" id="object-video">
+ <rect fill="&fill_color;" height="120" stroke="&stroke_color;" stroke-width="3.5" width="120" x="1.75" y="1.75"/>
+ <g>
+ <circle cx="17.667" cy="104.25" fill="#FFFFFF" r="2.5"/>
+ <circle cx="39.708" cy="104.25" fill="#FFFFFF" r="2.5"/>
+ <circle cx="61.75" cy="104.25" fill="#FFFFFF" r="5"/>
+ <circle cx="83.791" cy="104.25" fill="#FFFFFF" r="2.5"/>
+ <circle cx="105.834" cy="104.25" fill="#FFFFFF" r="2.5"/>
+ </g>
+ <rect fill="#EEEEEE" height="81" width="108" x="7.75" y="7.75"/>
+</g></svg> \ No newline at end of file
diff --git a/gfx/photoShutter.wav b/gfx/photoShutter.wav
new file mode 100644
index 0000000..08cbbf7
--- /dev/null
+++ b/gfx/photoShutter.wav
Binary files differ
diff --git a/gfx/xo-guy.svg b/gfx/xo-guy.svg
new file mode 100644
index 0000000..ef0c030
--- /dev/null
+++ b/gfx/xo-guy.svg
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 12.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 51448) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
+ <!ENTITY ns_svg "http://www.w3.org/2000/svg">
+ <!ENTITY ns_xlink "http://www.w3.org/1999/xlink">
+ <!ENTITY fill_color "#FFFFFF">
+ <!ENTITY stroke_color "#000000">
+]>
+<svg version="1.1" id="stock-buddy" xmlns="&ns_svg;" xmlns:xlink="&ns_xlink;" width="75" height="75" viewBox="0 0 75 75"
+ overflow="visible" enable-background="new 0 0 75 75" xml:space="preserve">
+<path fill="&fill_color;" stroke="&stroke_color;" stroke-width="3.5" d="M43.362,44.113l10.1,10.1c0.751,0.75,1.216,1.785,1.216,2.932
+ c0,2.286-1.856,4.143-4.146,4.143c-1.146,0-2.179-0.463-2.933-1.212L37.5,49.975L27.4,60.075c-0.75,0.749-1.787,1.21-2.933,1.21
+ c-2.285,0-4.142-1.854-4.142-4.141c0-1.146,0.465-2.182,1.212-2.932l10.103-10.1L21.538,34.01c-0.747-0.749-1.212-1.785-1.212-2.93
+ c0-2.289,1.853-4.145,4.145-4.145c1.143,0,2.18,0.465,2.93,1.214L37.5,38.25l10.1-10.101c0.754-0.749,1.787-1.214,2.933-1.214
+ c2.289,0,4.146,1.856,4.146,4.145c0,1.145-0.465,2.179-1.216,2.93L43.362,44.113z"/>
+<circle fill="&fill_color;" stroke="&stroke_color;" stroke-width="3.5" cx="37.5" cy="19.862" r="8.122"/>
+</svg>
diff --git a/glive.py b/glive.py
new file mode 100644
index 0000000..6464089
--- /dev/null
+++ b/glive.py
@@ -0,0 +1,645 @@
+#Copyright (c) 2008, Media Modifications Ltd.
+
+#Permission is hereby granted, free of charge, to any person obtaining a copy
+#of this software and associated documentation files (the "Software"), to deal
+#in the Software without restriction, including without limitation the rights
+#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+#copies of the Software, and to permit persons to whom the Software is
+#furnished to do so, subject to the following conditions:
+
+#The above copyright notice and this permission notice shall be included in
+#all copies or substantial portions of the Software.
+
+#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+#THE SOFTWARE.
+
+import os
+from gettext import gettext as _
+import time
+
+import gtk
+import gst
+import pygst
+pygst.require('0.10')
+import gobject
+gobject.threads_init()
+
+from sugar.activity.activity import get_bundle_path
+import logging
+
+from instance import Instance
+import constants
+import utils
+
+logger = logging.getLogger('glive')
+
+OGG_TRAITS = {
+ 0: { 'width': 160, 'height': 120, 'quality': 16 },
+ 1: { 'width': 400, 'height': 300, 'quality': 16 } }
+
+class Glive:
+ PHOTO_MODE_PHOTO = 0
+ PHOTO_MODE_AUDIO = 1
+
+ def __init__(self, activity_obj, model):
+ self.activity = activity_obj
+ self.model = model
+ self._eos_cb = None
+
+ self._has_camera = False
+ self._can_limit_framerate = False
+ self._playing = False
+ self._pic_exposure_open = False
+ self._thumb_exposure_open = False
+ self._photo_mode = self.PHOTO_MODE_PHOTO
+
+ self._audio_transcode_handler = None
+ self._transcode_id = None
+ self._video_transcode_handler = None
+ self._thumb_handoff_handler = None
+
+ self._audio_pixbuf = None
+
+ self._detect_camera()
+
+ self._pipeline = gst.Pipeline("Record")
+ self._create_photobin()
+ self._create_audiobin()
+ self._create_videobin()
+ self._create_xbin()
+ self._create_pipeline()
+
+ self._thumb_pipes = []
+ self._mux_pipes = []
+
+ bus = self._pipeline.get_bus()
+ bus.add_signal_watch()
+ bus.connect('message', self._bus_message_handler)
+
+ def _detect_camera(self):
+ v4l2src = gst.element_factory_make('v4l2src')
+ if v4l2src.props.device_name is None:
+ return
+
+ self._has_camera = True
+
+ # Figure out if we can place a framerate limit on the v4l2 element,
+ # which in theory will make it all the way down to the hardware.
+ # ideally, we should be able to do this by checking caps. However, I
+ # can't find a way to do this (at this time, XO-1 cafe camera driver
+ # doesn't support framerate changes, but gstreamer caps suggest
+ # otherwise)
+ pipeline = gst.Pipeline()
+ caps = gst.Caps('video/x-raw-yuv,framerate=10/1')
+ fsink = gst.element_factory_make('fakesink')
+ pipeline.add(v4l2src, fsink)
+ v4l2src.link(fsink, caps)
+ self._can_limit_framerate = pipeline.set_state(gst.STATE_PAUSED) != gst.STATE_CHANGE_FAILURE
+ pipeline.set_state(gst.STATE_NULL)
+
+ def get_has_camera(self):
+ return self._has_camera
+
+ def _create_photobin(self):
+ queue = gst.element_factory_make("queue", "pbqueue")
+ queue.set_property("leaky", True)
+ queue.set_property("max-size-buffers", 1)
+
+ colorspace = gst.element_factory_make("ffmpegcolorspace", "pbcolorspace")
+ jpeg = gst.element_factory_make("jpegenc", "pbjpeg")
+
+ sink = gst.element_factory_make("fakesink", "pbsink")
+ sink.connect("handoff", self._photo_handoff)
+ sink.set_property("signal-handoffs", True)
+
+ self._photobin = gst.Bin("photobin")
+ self._photobin.add(queue, colorspace, jpeg, sink)
+
+ gst.element_link_many(queue, colorspace, jpeg, sink)
+
+ pad = queue.get_static_pad("sink")
+ self._photobin.add_pad(gst.GhostPad("sink", pad))
+
+ def _create_audiobin(self):
+ src = gst.element_factory_make("alsasrc", "absrc")
+
+ # attempt to use direct access to the 0,0 device, solving some A/V
+ # sync issues
+ src.set_property("device", "plughw:0,0")
+ hwdev_available = src.set_state(gst.STATE_PAUSED) != gst.STATE_CHANGE_FAILURE
+ src.set_state(gst.STATE_NULL)
+ if not hwdev_available:
+ src.set_property("device", "default")
+
+ srccaps = gst.Caps("audio/x-raw-int,rate=16000,channels=1,depth=16")
+
+ # guarantee perfect stream, important for A/V sync
+ rate = gst.element_factory_make("audiorate")
+
+ # without a buffer here, gstreamer struggles at the start of the
+ # recording and then the A/V sync is bad for the whole video
+ # (possibly a gstreamer/ALSA bug -- even if it gets caught up, it
+ # should be able to resync without problem)
+ queue = gst.element_factory_make("queue", "audioqueue")
+ queue.set_property("leaky", True) # prefer fresh data
+ queue.set_property("max-size-time", 5000000000) # 5 seconds
+ queue.set_property("max-size-buffers", 500)
+ queue.connect("overrun", self._log_queue_overrun)
+
+ enc = gst.element_factory_make("wavenc", "abenc")
+
+ sink = gst.element_factory_make("filesink", "absink")
+ sink.set_property("location", os.path.join(Instance.instancePath, "output.wav"))
+
+ self._audiobin = gst.Bin("audiobin")
+ self._audiobin.add(src, rate, queue, enc, sink)
+
+ src.link(rate, srccaps)
+ gst.element_link_many(rate, queue, enc, sink)
+
+ def _create_videobin(self):
+ queue = gst.element_factory_make("queue", "videoqueue")
+ queue.set_property("max-size-time", 5000000000) # 5 seconds
+ queue.set_property("max-size-bytes", 33554432) # 32mb
+ queue.connect("overrun", self._log_queue_overrun)
+
+ scale = gst.element_factory_make("videoscale", "vbscale")
+
+ scalecapsfilter = gst.element_factory_make("capsfilter", "scalecaps")
+
+ scalecaps = gst.Caps('video/x-raw-yuv,width=160,height=120')
+ scalecapsfilter.set_property("caps", scalecaps)
+
+ colorspace = gst.element_factory_make("ffmpegcolorspace", "vbcolorspace")
+
+ enc = gst.element_factory_make("theoraenc", "vbenc")
+ enc.set_property("quality", 16)
+
+ mux = gst.element_factory_make("oggmux", "vbmux")
+
+ sink = gst.element_factory_make("filesink", "vbfile")
+ sink.set_property("location", os.path.join(Instance.instancePath, "output.ogg"))
+
+ self._videobin = gst.Bin("videobin")
+ self._videobin.add(queue, scale, scalecapsfilter, colorspace, enc, mux, sink)
+
+ queue.link(scale)
+ scale.link_pads(None, scalecapsfilter, "sink")
+ scalecapsfilter.link_pads("src", colorspace, None)
+ gst.element_link_many(colorspace, enc, mux, sink)
+
+ pad = queue.get_static_pad("sink")
+ self._videobin.add_pad(gst.GhostPad("sink", pad))
+
+ def _create_xbin(self):
+ scale = gst.element_factory_make("videoscale")
+ cspace = gst.element_factory_make("ffmpegcolorspace")
+ xsink = gst.element_factory_make("ximagesink", "xsink")
+ xsink.set_property("force-aspect-ratio", True)
+
+ # http://thread.gmane.org/gmane.comp.video.gstreamer.devel/29644
+ xsink.set_property("sync", False)
+
+ self._xbin = gst.Bin("xbin")
+ self._xbin.add(scale, cspace, xsink)
+ gst.element_link_many(scale, cspace, xsink)
+
+ pad = scale.get_static_pad("sink")
+ self._xbin.add_pad(gst.GhostPad("sink", pad))
+
+ def _config_videobin(self, quality, width, height):
+ vbenc = self._videobin.get_by_name("vbenc")
+ vbenc.set_property("quality", 16)
+ scaps = self._videobin.get_by_name("scalecaps")
+ scaps.set_property("caps", gst.Caps("video/x-raw-yuv,width=%d,height=%d" % (width, height)))
+
+ def _create_pipeline(self):
+ if not self._has_camera:
+ return
+
+ src = gst.element_factory_make("v4l2src", "camsrc")
+ try:
+ # old gst-plugins-good does not have this property
+ src.set_property("queue-size", 2)
+ except:
+ pass
+
+ # if possible, it is important to place the framerate limit directly
+ # on the v4l2src so that it gets communicated all the way down to the
+ # camera level
+ if self._can_limit_framerate:
+ srccaps = gst.Caps('video/x-raw-yuv,framerate=10/1')
+ else:
+ srccaps = gst.Caps('video/x-raw-yuv')
+
+ # we attempt to limit the framerate on the v4l2src directly, but we
+ # can't trust this: perhaps we are falling behind in our capture,
+ # or maybe the kernel driver doesn't provide the exact framerate.
+ # the videorate element guarantees a perfect framerate and is important
+ # for A/V sync because OGG does not store timestamps, it just stores
+ # the FPS value.
+ rate = gst.element_factory_make("videorate")
+ ratecaps = gst.Caps('video/x-raw-yuv,framerate=10/1')
+
+ tee = gst.element_factory_make("tee", "tee")
+ queue = gst.element_factory_make("queue", "dispqueue")
+
+ # prefer fresh frames
+ queue.set_property("leaky", True)
+ queue.set_property("max-size-buffers", 2)
+
+ self._pipeline.add(src, rate, tee, queue)
+ src.link(rate, srccaps)
+ rate.link(tee, ratecaps)
+ tee.link(queue)
+
+ self._xvsink = gst.element_factory_make("xvimagesink", "xsink")
+ self._xv_available = self._xvsink.set_state(gst.STATE_PAUSED) != gst.STATE_CHANGE_FAILURE
+ self._xvsink.set_state(gst.STATE_NULL)
+
+ # http://thread.gmane.org/gmane.comp.video.gstreamer.devel/29644
+ self._xvsink.set_property("sync", False)
+
+ self._xvsink.set_property("force-aspect-ratio", True)
+
+ def _log_queue_overrun(self, queue):
+ cbuffers = queue.get_property("current-level-buffers")
+ cbytes = queue.get_property("current-level-bytes")
+ ctime = queue.get_property("current-level-time")
+ logger.error("Buffer overrun in %s (%d buffers, %d bytes, %d time)"
+ % (queue.get_name(), cbuffers, cbytes, ctime))
+
+ def _thumb_element(self, name):
+ return self._thumb_pipes[-1].get_by_name(name)
+
+ def is_using_xv(self):
+ return self._pipeline.get_by_name("xsink") == self._xvsink
+
+ def _configure_xv(self):
+ if self.is_using_xv():
+ # nothing to do, Xv already configured
+ return self._xvsink
+
+ queue = self._pipeline.get_by_name("dispqueue")
+ if self._pipeline.get_by_name("xbin"):
+ # X sink is configured, so remove it
+ queue.unlink(self._xbin)
+ self._pipeline.remove(self._xbin)
+
+ self._pipeline.add(self._xvsink)
+ queue.link(self._xvsink)
+ return self._xvsink
+
+ def _configure_x(self):
+ if self._pipeline.get_by_name("xbin") == self._xbin:
+ # nothing to do, X already configured
+ return self._xbin.get_by_name("xsink")
+
+ queue = self._pipeline.get_by_name("dispqueue")
+ xvsink = self._pipeline.get_by_name("xsink")
+
+ if xvsink:
+ # Xv sink is configured, so remove it
+ queue.unlink(xvsink)
+ self._pipeline.remove(xvsink)
+
+ self._pipeline.add(self._xbin)
+ queue.link(self._xbin)
+ return self._xbin.get_by_name("xsink")
+
+ def play(self, use_xv=True):
+ if self._get_state() == gst.STATE_PLAYING:
+ return
+
+ if self._has_camera:
+ if use_xv and self._xv_available:
+ xsink = self._configure_xv()
+ else:
+ xsink = self._configure_x()
+
+ # X overlay must be set every time, it seems to forget when you stop
+ # the pipeline.
+ self.activity.set_glive_sink(xsink)
+
+ self._pipeline.set_state(gst.STATE_PLAYING)
+ self._playing = True
+
+ def pause(self):
+ self._pipeline.set_state(gst.STATE_PAUSED)
+ self._playing = False
+
+ def stop(self):
+ self._pipeline.set_state(gst.STATE_NULL)
+ self._playing = False
+
+ def is_playing(self):
+ return self._playing
+
+ def _get_state(self):
+ return self._pipeline.get_state()[1]
+
+ def stop_recording_audio(self):
+ # We should be able to simply pause and remove the audiobin, but
+ # this seems to cause a gstreamer segfault. So we stop the whole
+ # pipeline while manipulating it.
+ # http://dev.laptop.org/ticket/10183
+ self._pipeline.set_state(gst.STATE_NULL)
+ self.model.shutter_sound()
+ self._pipeline.remove(self._audiobin)
+ self.play()
+
+ audio_path = os.path.join(Instance.instancePath, "output.wav")
+ if not os.path.exists(audio_path) or os.path.getsize(audio_path) <= 0:
+ # FIXME: inform model of failure?
+ return
+
+ if self._audio_pixbuf:
+ self.model.still_ready(self._audio_pixbuf)
+
+ line = 'filesrc location=' + audio_path + ' name=audioFilesrc ! wavparse name=audioWavparse ! audioconvert name=audioAudioconvert ! vorbisenc name=audioVorbisenc ! oggmux name=audioOggmux ! filesink name=audioFilesink'
+ audioline = gst.parse_launch(line)
+
+ taglist = self._get_tags(constants.TYPE_AUDIO)
+
+ if self._audio_pixbuf:
+ pixbuf_b64 = utils.getStringFromPixbuf(self._audio_pixbuf)
+ taglist[gst.TAG_EXTENDED_COMMENT] = "coverart=" + pixbuf_b64
+
+ vorbis_enc = audioline.get_by_name('audioVorbisenc')
+ vorbis_enc.merge_tags(taglist, gst.TAG_MERGE_REPLACE_ALL)
+
+ audioFilesink = audioline.get_by_name('audioFilesink')
+ audioOggFilepath = os.path.join(Instance.instancePath, "output.ogg")
+ audioFilesink.set_property("location", audioOggFilepath)
+
+ audioBus = audioline.get_bus()
+ audioBus.add_signal_watch()
+ self._audio_transcode_handler = audioBus.connect('message', self._onMuxedAudioMessageCb, audioline)
+ self._transcode_id = gobject.timeout_add(200, self._transcodeUpdateCb, audioline)
+ audioline.set_state(gst.STATE_PLAYING)
+
+ def _get_tags(self, type):
+ tl = gst.TagList()
+ tl[gst.TAG_ARTIST] = self.model.get_nickname()
+ tl[gst.TAG_COMMENT] = "olpc"
+ #this is unfortunately, unreliable
+ #record.Record.log.debug("self.ca.metadata['title']->" + str(self.ca.metadata['title']) )
+ tl[gst.TAG_ALBUM] = "olpc" #self.ca.metadata['title']
+ tl[gst.TAG_DATE] = utils.getDateString(int(time.time()))
+ stringType = constants.MEDIA_INFO[type]['istr']
+
+ # Translators: photo by photographer, e.g. "Photo by Mary"
+ tl[gst.TAG_TITLE] = _('%s by %s') % (stringType, self.model.get_nickname())
+ return tl
+
+ def blockedCb(self, x, y, z):
+ pass
+
+ def _take_photo(self, photo_mode):
+ if self._pic_exposure_open:
+ return
+
+ self._photo_mode = photo_mode
+ self._pic_exposure_open = True
+ pad = self._photobin.get_static_pad("sink")
+ pad.set_blocked_async(True, self.blockedCb, None)
+ self._pipeline.add(self._photobin)
+ self._photobin.set_state(gst.STATE_PLAYING)
+ self._pipeline.get_by_name("tee").link(self._photobin)
+ pad.set_blocked_async(False, self.blockedCb, None)
+
+ def take_photo(self):
+ if self._has_camera:
+ self._take_photo(self.PHOTO_MODE_PHOTO)
+
+ def _photo_handoff(self, fsink, buffer, pad, user_data=None):
+ if not self._pic_exposure_open:
+ return
+
+ pad = self._photobin.get_static_pad("sink")
+ pad.set_blocked_async(True, self.blockedCb, None)
+ self._pipeline.get_by_name("tee").unlink(self._photobin)
+ self._pipeline.remove(self._photobin)
+ pad.set_blocked_async(False, self.blockedCb, None)
+
+ self._pic_exposure_open = False
+ pic = gtk.gdk.pixbuf_loader_new_with_mime_type("image/jpeg")
+ pic.write( buffer )
+ pic.close()
+ pixBuf = pic.get_pixbuf()
+ del pic
+
+ self.save_photo(pixBuf)
+
+ def save_photo(self, pixbuf):
+ if self._photo_mode == self.PHOTO_MODE_AUDIO:
+ self._audio_pixbuf = pixbuf
+ else:
+ self.model.save_photo(pixbuf)
+
+ def record_video(self, quality):
+ if not self._has_camera:
+ return
+
+ self._ogg_quality = quality
+ self._config_videobin(OGG_TRAITS[quality]['quality'],
+ OGG_TRAITS[quality]['width'],
+ OGG_TRAITS[quality]['height'])
+
+ # If we use pad blocking and adjust the pipeline on-the-fly, the
+ # resultant video has bad A/V sync :(
+ # If we pause the pipeline while adjusting it, the A/V sync is better
+ # but not perfect :(
+ # so we stop the whole thing while reconfiguring to get the best results
+ self._pipeline.set_state(gst.STATE_NULL)
+ self._pipeline.add(self._videobin)
+ self._pipeline.get_by_name("tee").link(self._videobin)
+ self._pipeline.add(self._audiobin)
+ self.play()
+
+ def record_audio(self):
+ if self._has_camera:
+ self._audio_pixbuf = None
+ self._take_photo(self.PHOTO_MODE_AUDIO)
+
+ # we should be able to add the audiobin on the fly, but unfortunately
+ # this results in several seconds of silence being added at the start
+ # of the recording. So we stop the whole pipeline while adjusting it.
+ # SL#2040
+ self._pipeline.set_state(gst.STATE_NULL)
+ self._pipeline.add(self._audiobin)
+ self.play()
+
+ def stop_recording_video(self):
+ if not self._has_camera:
+ return
+
+ # We stop the pipeline while we are adjusting the pipeline to stop
+ # recording because if we do it on-the-fly, the following video live
+ # feed to the screen becomes several seconds delayed. Weird!
+ # FIXME: retest on F11
+ # FIXME: could this be the result of audio shortening problems?
+ self._eos_cb = self._video_eos
+ self._pipeline.get_by_name('camsrc').send_event(gst.event_new_eos())
+ self._audiobin.get_by_name('absrc').send_event(gst.event_new_eos())
+
+ def _video_eos(self):
+ self._pipeline.set_state(gst.STATE_NULL)
+ self._pipeline.get_by_name("tee").unlink(self._videobin)
+ self._pipeline.remove(self._videobin)
+ self._pipeline.remove(self._audiobin)
+
+ self.model.shutter_sound()
+
+ if len(self._thumb_pipes) > 0:
+ thumbline = self._thumb_pipes[-1]
+ thumbline.get_by_name('thumb_fakesink').disconnect(self._thumb_handoff_handler)
+
+ ogg_path = os.path.join(Instance.instancePath, "output.ogg") #ogv
+ if not os.path.exists(ogg_path) or os.path.getsize(ogg_path) <= 0:
+ # FIXME: inform model of failure?
+ return
+
+ line = 'filesrc location=' + ogg_path + ' name=thumbFilesrc ! oggdemux name=thumbOggdemux ! theoradec name=thumbTheoradec ! tee name=thumb_tee ! queue name=thumb_queue ! ffmpegcolorspace name=thumbFfmpegcolorspace ! jpegenc name=thumbJPegenc ! fakesink name=thumb_fakesink'
+ thumbline = gst.parse_launch(line)
+ thumb_queue = thumbline.get_by_name('thumb_queue')
+ thumb_queue.set_property("leaky", True)
+ thumb_queue.set_property("max-size-buffers", 1)
+ thumb_tee = thumbline.get_by_name('thumb_tee')
+ thumb_fakesink = thumbline.get_by_name('thumb_fakesink')
+ self._thumb_handoff_handler = thumb_fakesink.connect("handoff", self.copyThumbPic)
+ thumb_fakesink.set_property("signal-handoffs", True)
+ self._thumb_pipes.append(thumbline)
+ self._thumb_exposure_open = True
+ thumbline.set_state(gst.STATE_PLAYING)
+
+ def copyThumbPic(self, fsink, buffer, pad, user_data=None):
+ if not self._thumb_exposure_open:
+ return
+
+ self._thumb_exposure_open = False
+ loader = gtk.gdk.pixbuf_loader_new_with_mime_type("image/jpeg")
+ loader.write(buffer)
+ loader.close()
+ self.thumbBuf = loader.get_pixbuf()
+ self.model.still_ready(self.thumbBuf)
+
+ self._thumb_element('thumb_tee').unlink(self._thumb_element('thumb_queue'))
+
+ oggFilepath = os.path.join(Instance.instancePath, "output.ogg") #ogv
+ wavFilepath = os.path.join(Instance.instancePath, "output.wav")
+ muxFilepath = os.path.join(Instance.instancePath, "mux.ogg") #ogv
+
+ muxline = gst.parse_launch('filesrc location=' + str(oggFilepath) + ' name=muxVideoFilesrc ! oggdemux name=muxOggdemux ! theoraparse ! oggmux name=muxOggmux ! filesink location=' + str(muxFilepath) + ' name=muxFilesink filesrc location=' + str(wavFilepath) + ' name=muxAudioFilesrc ! wavparse name=muxWavparse ! audioconvert name=muxAudioconvert ! vorbisenc name=muxVorbisenc ! muxOggmux.')
+ taglist = self._get_tags(constants.TYPE_VIDEO)
+ vorbis_enc = muxline.get_by_name('muxVorbisenc')
+ vorbis_enc.merge_tags(taglist, gst.TAG_MERGE_REPLACE_ALL)
+
+ muxBus = muxline.get_bus()
+ muxBus.add_signal_watch()
+ self._video_transcode_handler = muxBus.connect('message', self._onMuxedVideoMessageCb, muxline)
+ self._mux_pipes.append(muxline)
+ #add a listener here to monitor % of transcoding...
+ self._transcode_id = gobject.timeout_add(200, self._transcodeUpdateCb, muxline)
+ muxline.set_state(gst.STATE_PLAYING)
+
+ def _transcodeUpdateCb( self, pipe ):
+ position, duration = self._query_position( pipe )
+ if position != gst.CLOCK_TIME_NONE:
+ value = position * 100.0 / duration
+ value = value/100.0
+ self.model.set_progress(value, _('Saving...'))
+ return True
+
+ def _query_position(self, pipe):
+ try:
+ position, format = pipe.query_position(gst.FORMAT_TIME)
+ except:
+ position = gst.CLOCK_TIME_NONE
+
+ try:
+ duration, format = pipe.query_duration(gst.FORMAT_TIME)
+ except:
+ duration = gst.CLOCK_TIME_NONE
+
+ return (position, duration)
+
+ def _onMuxedVideoMessageCb(self, bus, message, pipe):
+ if message.type != gst.MESSAGE_EOS:
+ return True
+
+ gobject.source_remove(self._video_transcode_handler)
+ self._video_transcode_handler = None
+ gobject.source_remove(self._transcode_id)
+ self._transcode_id = None
+ pipe.set_state(gst.STATE_NULL)
+ pipe.get_bus().remove_signal_watch()
+ pipe.get_bus().disable_sync_message_emission()
+
+ wavFilepath = os.path.join(Instance.instancePath, "output.wav")
+ oggFilepath = os.path.join(Instance.instancePath, "output.ogg") #ogv
+ muxFilepath = os.path.join(Instance.instancePath, "mux.ogg") #ogv
+ os.remove( wavFilepath )
+ os.remove( oggFilepath )
+ self.model.save_video(muxFilepath, self.thumbBuf)
+ return False
+
+ def _onMuxedAudioMessageCb(self, bus, message, pipe):
+ if message.type != gst.MESSAGE_EOS:
+ return True
+
+ gobject.source_remove(self._audio_transcode_handler)
+ self._audio_transcode_handler = None
+ gobject.source_remove(self._transcode_id)
+ self._transcode_id = None
+ pipe.set_state(gst.STATE_NULL)
+ pipe.get_bus().remove_signal_watch()
+ pipe.get_bus().disable_sync_message_emission()
+
+ wavFilepath = os.path.join(Instance.instancePath, "output.wav")
+ oggFilepath = os.path.join(Instance.instancePath, "output.ogg")
+ os.remove( wavFilepath )
+ self.model.save_audio(oggFilepath, self._audio_pixbuf)
+ return False
+
+ def _bus_message_handler(self, bus, message):
+ t = message.type
+ if t == gst.MESSAGE_EOS:
+ if self._eos_cb:
+ cb = self._eos_cb
+ self._eos_cb = None
+ cb()
+ elif t == gst.MESSAGE_ERROR:
+ #todo: if we come out of suspend/resume with errors, then get us back up and running...
+ #todo: handle "No space left on the resource.gstfilesink.c"
+ #err, debug = message.parse_error()
+ pass
+
+ def abandonMedia(self):
+ self.stop()
+
+ if self._audio_transcode_handler:
+ gobject.source_remove(self._audio_transcode_handler)
+ self._audio_transcode_handler = None
+ if self._transcode_id:
+ gobject.source_remove(self._transcode_id)
+ self._transcode_id = None
+ if self._video_transcode_handler:
+ gobject.source_remove(self._video_transcode_handler)
+ self._video_transcode_handler = None
+
+ wav_path = os.path.join(Instance.instancePath, "output.wav")
+ if os.path.exists(wav_path):
+ os.remove(wav_path)
+ ogg_path = os.path.join(Instance.instancePath, "output.ogg") #ogv
+ if os.path.exists(ogg_path):
+ os.remove(ogg_path)
+ mux_path = os.path.join(Instance.instancePath, "mux.ogg") #ogv
+ if os.path.exists(mux_path):
+ os.remove(mux_path)
+
diff --git a/glive.pyc b/glive.pyc
new file mode 100644
index 0000000..a3340ae
--- /dev/null
+++ b/glive.pyc
Binary files differ
diff --git a/gplay.py b/gplay.py
new file mode 100644
index 0000000..83e5340
--- /dev/null
+++ b/gplay.py
@@ -0,0 +1,116 @@
+#Copyright (c) 2008, Media Modifications Ltd.
+
+#Permission is hereby granted, free of charge, to any person obtaining a copy
+#of this software and associated documentation files (the "Software"), to deal
+#in the Software without restriction, including without limitation the rights
+#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+#copies of the Software, and to permit persons to whom the Software is
+#furnished to do so, subject to the following conditions:
+
+ #The above copyright notice and this permission notice shall be included in
+#all copies or substantial portions of the Software.
+
+#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+#THE SOFTWARE.
+
+import gobject
+gobject.threads_init()
+import pygst
+pygst.require('0.10')
+import gst
+
+import logging
+logger = logging.getLogger('record:gplay.py')
+
+class Gplay(gobject.GObject):
+ __gsignals__ = {
+ 'playback-status-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_INT, gobject.TYPE_FLOAT)),
+ }
+
+ def __init__(self, activity_obj):
+ super(Gplay, self).__init__()
+ self.activity = activity_obj
+ self._playback_monitor_handler = None
+ self._player = gst.element_factory_make('playbin')
+
+ bus = self._player.get_bus()
+ bus.add_signal_watch()
+ bus.connect('message::error', self._bus_error)
+ bus.connect('message::eos', self._bus_eos)
+
+ def _bus_error(self, bus, message):
+ err, debug = message.parse_error()
+ logger.error('bus error=%s debug=%s' % (err, debug))
+
+ def _bus_eos(self, bus, message):
+ self.stop()
+
+ def set_location(self, location):
+ if self._player.get_property('uri') == location:
+ self.seek(0)
+ return
+
+ self._player.set_state(gst.STATE_READY)
+ self._player.set_property('uri', location)
+
+ def seek(self, position):
+ if position == 0:
+ location = 0
+ else:
+ duration = self._player.query_duration(gst.FORMAT_TIME, None)[0]
+ location = duration * (position / 100)
+
+ event = gst.event_new_seek(1.0, gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_ACCURATE, gst.SEEK_TYPE_SET, location, gst.SEEK_TYPE_NONE, 0)
+ res = self._player.send_event(event)
+ if res:
+ self._player.set_new_stream_time(0L)
+
+ def pause(self):
+ self._player.set_state(gst.STATE_PAUSED)
+
+ def play(self):
+ if self.get_state() == gst.STATE_PLAYING:
+ return
+
+ if not self._player.props.video_sink:
+ sink = gst.element_factory_make('xvimagesink')
+ sink.props.force_aspect_ratio = True
+ self._player.props.video_sink = sink
+
+ self.activity.set_gplay_sink(self._player.props.video_sink)
+ self._player.set_state(gst.STATE_PLAYING)
+ self._emit_playback_status(0)
+
+ self._playback_monitor_handler = gobject.timeout_add(500, self._playback_monitor)
+
+ def _playback_monitor(self):
+ try:
+ position = self._player.query_position(gst.FORMAT_TIME)[0]
+ duration = self._player.query_duration(gst.FORMAT_TIME)[0]
+ except gst.QueryError:
+ return True
+
+ value = (float(position) / float(duration)) * 100.0
+ self._emit_playback_status(value)
+ return True
+
+ def _emit_playback_status(self, position):
+ state = self._player.get_state()[1]
+ self.emit('playback-status-changed', state, position)
+
+ def get_state(self):
+ return self._player.get_state()[1]
+
+ def stop(self):
+ if self._playback_monitor_handler:
+ gobject.source_remove(self._playback_monitor_handler)
+ self._playback_monitor_handler = None
+
+ self._player.set_state(gst.STATE_NULL)
+ self._emit_playback_status(0)
+
diff --git a/gplay.pyc b/gplay.pyc
new file mode 100644
index 0000000..f0c22e3
--- /dev/null
+++ b/gplay.pyc
Binary files differ
diff --git a/instance.py b/instance.py
new file mode 100644
index 0000000..bcee466
--- /dev/null
+++ b/instance.py
@@ -0,0 +1,22 @@
+import os
+
+from sugar import profile
+from sugar import util
+
+class Instance:
+ key = profile.get_pubkey()
+ keyHash = util.sha_data(key)
+
+ keyHashPrintable = util.printable_hash(keyHash)
+
+ instancePath = None
+
+ def __init__(self, ca):
+ self.__class__.instancePath = os.path.join(ca.get_activity_root(), "instance")
+ recreateTmp()
+
+
+def recreateTmp():
+ if (not os.path.exists(Instance.instancePath)):
+ os.makedirs(Instance.instancePath)
+
diff --git a/instance.pyc b/instance.pyc
new file mode 100644
index 0000000..6ec3af7
--- /dev/null
+++ b/instance.pyc
Binary files differ
diff --git a/liitle-tramp.e4p b/liitle-tramp.e4p
new file mode 100644
index 0000000..9a07bad
--- /dev/null
+++ b/liitle-tramp.e4p
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE Project SYSTEM "Project-4.6.dtd">
+<!-- eric4 project file for project liitle-tramp -->
+<!-- Saved: 2012-03-30, 11:08:11 -->
+<!-- Copyright (C) 2012 , -->
+<Project version="4.6">
+ <Language>en</Language>
+ <ProgLanguage mixed="0">Python</ProgLanguage>
+ <ProjectType>Qt4</ProjectType>
+ <Description></Description>
+ <Version>0.1</Version>
+ <Author></Author>
+ <Email></Email>
+ <Sources>
+ <Source>activity.py</Source>
+ <Source>model.py</Source>
+ <Source>tray.py</Source>
+ <Source>setup.py</Source>
+ <Source>serialize.py</Source>
+ <Source>recorded.py</Source>
+ <Source>mediaview.py</Source>
+ <Source>treeview.py</Source>
+ <Source>glive.py</Source>
+ <Source>constants.py</Source>
+ <Source>collab.py</Source>
+ <Source>utils.py</Source>
+ <Source>record.py</Source>
+ <Source>gplay.py</Source>
+ <Source>button.py</Source>
+ <Source>instance.py</Source>
+ <Source>__init__.py</Source>
+ </Sources>
+ <Forms>
+ </Forms>
+ <Translations>
+ </Translations>
+ <Resources>
+ </Resources>
+ <Interfaces>
+ </Interfaces>
+ <Others>
+ </Others>
+ <MainScript>activity.py</MainScript>
+ <Vcs>
+ <VcsType>None</VcsType>
+ </Vcs>
+ <FiletypeAssociations>
+ <FiletypeAssociation pattern="*.ui" type="FORMS" />
+ <FiletypeAssociation pattern="*.idl" type="INTERFACES" />
+ <FiletypeAssociation pattern="*.qm" type="TRANSLATIONS" />
+ <FiletypeAssociation pattern="*.ptl" type="SOURCES" />
+ <FiletypeAssociation pattern="*.pyw" type="SOURCES" />
+ <FiletypeAssociation pattern="*.ui.h" type="FORMS" />
+ <FiletypeAssociation pattern="*.ts" type="TRANSLATIONS" />
+ <FiletypeAssociation pattern="*.py" type="SOURCES" />
+ <FiletypeAssociation pattern="*.qrc" type="RESOURCES" />
+ </FiletypeAssociations>
+</Project> \ No newline at end of file
diff --git a/mediaview.py b/mediaview.py
new file mode 100644
index 0000000..01567ff
--- /dev/null
+++ b/mediaview.py
@@ -0,0 +1,515 @@
+import os
+from gettext import gettext as _
+
+import gobject
+import gtk
+from gtk import gdk
+
+import constants
+import utils
+
+COLOR_BLACK = gdk.color_parse('#000000')
+COLOR_WHITE = gdk.color_parse('#ffffff')
+COLOR_GREY = gdk.color_parse('#808080')
+
+class XoIcon(gtk.Image):
+ def __init__(self):
+ super(XoIcon, self).__init__()
+
+ def set_colors(self, stroke, fill):
+ pixbuf = utils.load_colored_svg('xo-guy.svg', stroke, fill)
+ self.set_from_pixbuf(pixbuf)
+
+
+class InfoView(gtk.EventBox):
+ """
+ A metadata view/edit widget, that presents a primary view area in the top
+ right and a secondary view area in the bottom left.
+ """
+ __gsignals__ = {
+ 'primary-allocated': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+ 'secondary-allocated': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+ 'tags-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_OBJECT,)),
+ }
+
+ def __init__(self):
+ super(InfoView, self).__init__()
+ self.modify_bg(gtk.STATE_NORMAL, COLOR_GREY)
+
+ self.connect('size-allocate', self._size_allocate)
+
+ self._outer_vbox = gtk.VBox(spacing=7)
+ self.add(self._outer_vbox)
+
+ hbox = gtk.HBox()
+ self._outer_vbox.pack_start(hbox, expand=True, fill=True)
+
+ inner_vbox = gtk.VBox(spacing=5)
+ hbox.pack_start(inner_vbox, expand=True, fill=True, padding=6)
+
+ author_hbox = gtk.HBox()
+ inner_vbox.pack_start(author_hbox, expand=False)
+
+ label = gtk.Label()
+ label.set_markup('<b>' + _('Author:') + '</b>')
+ author_hbox.pack_start(label, expand=False)
+
+ self._xo_icon = XoIcon()
+ author_hbox.pack_start(self._xo_icon, expand=False)
+
+ self._author_label = gtk.Label()
+ author_hbox.pack_start(self._author_label, expand=False)
+
+ self._date_label = gtk.Label()
+ self._date_label.set_line_wrap(True)
+ alignment = gtk.Alignment(0.0, 0.5, 0.0, 0.0)
+ alignment.add(self._date_label)
+ inner_vbox.pack_start(alignment, expand=False)
+
+ label = gtk.Label()
+ label.set_markup('<b>' + _('Tags:') + '</b>')
+ alignment = gtk.Alignment(0.0, 0.5, 0.0, 0.0)
+ alignment.add(label)
+ inner_vbox.pack_start(alignment, expand=False)
+
+ textview = gtk.TextView()
+ self._tags_buffer = textview.get_buffer()
+ self._tags_buffer.connect('changed', self._tags_changed)
+ inner_vbox.pack_start(textview, expand=True, fill=True)
+
+ # the main viewing widget will be painted exactly on top of this one
+ alignment = gtk.Alignment(1.0, 0.0, 0.0, 0.0)
+ self._view_bg = gtk.EventBox()
+ self._view_bg.modify_bg(gtk.STATE_NORMAL, COLOR_BLACK)
+ alignment.add(self._view_bg)
+ hbox.pack_start(alignment, expand=False)
+
+ # the secondary viewing widget will be painted exactly on top of this one
+ alignment = gtk.Alignment(0.0, 1.0, 0.0, 0.0)
+ self._live_bg = gtk.EventBox()
+ self._live_bg.modify_bg(gtk.STATE_NORMAL, COLOR_BLACK)
+ alignment.add(self._live_bg)
+ self._outer_vbox.pack_start(alignment, expand=False)
+
+ def fit_to_allocation(self, allocation):
+ # main viewing area: 50% of each dimension
+ scale = 0.5
+ w = int(allocation.width * scale)
+ h = int(allocation.height * scale)
+ self._view_bg.set_size_request(w, h)
+
+ # live area: 1/4 of each dimension
+ scale = 0.25
+ w = int(allocation.width * scale)
+ h = int(allocation.height * scale)
+ self._live_bg.set_size_request(w, h)
+
+ def show(self):
+ self.show_all()
+
+ def hide(self):
+ self.hide_all()
+
+ def set_author(self, name, stroke, fill):
+ self._xo_icon.set_colors(stroke, fill)
+ self._author_label.set_text(name)
+
+ def set_date(self, date):
+ self._date_label.set_markup('<b>' + _('Date:') + '</b> ' + date)
+
+ def set_tags(self, tags):
+ self._tags_buffer.set_text(tags)
+
+ def _size_allocate(self, widget, allocation):
+ self.emit('primary-allocated', self._view_bg.allocation)
+ self.emit('secondary-allocated', self._live_bg.allocation)
+
+ def _tags_changed(self, widget):
+ self.emit('tags-changed', widget)
+
+class VideoBox(gtk.EventBox):
+ """
+ A widget with its own window for rendering a gstreamer video sink onto.
+ """
+ def __init__(self):
+ super(VideoBox, self).__init__()
+ self.unset_flags(gtk.DOUBLE_BUFFERED)
+ self.set_flags(gtk.APP_PAINTABLE)
+ self._sink = None
+ self._xid = None
+ self.connect('realize', self._realize)
+
+ def _realize(self, widget):
+ self._xid = self.window.xid
+
+ def do_expose_event(self):
+ if self._sink:
+ self._sink.expose()
+ return False
+ else:
+ return True
+
+ # can be called from gstreamer thread, must not do any GTK+ stuff
+ def set_sink(self, sink):
+ self._sink = sink
+ sink.set_xwindow_id(self._xid)
+
+class FullscreenButton(gtk.EventBox):
+ def __init__(self):
+ super(FullscreenButton, self).__init__()
+
+ path = os.path.join(constants.GFX_PATH, 'max-reduce.svg')
+ self._enlarge_pixbuf = gdk.pixbuf_new_from_file(path)
+ self.width = self._enlarge_pixbuf.get_width()
+ self.height = self._enlarge_pixbuf.get_height()
+
+ path = os.path.join(constants.GFX_PATH, 'max-enlarge.svg')
+ self._reduce_pixbuf = gdk.pixbuf_new_from_file_at_size(path, self.width, self.height)
+
+ self._image = gtk.Image()
+ self.set_enlarge()
+ self._image.show()
+ self.add(self._image)
+
+ def set_enlarge(self):
+ self._image.set_from_pixbuf(self._enlarge_pixbuf)
+
+ def set_reduce(self):
+ self._image.set_from_pixbuf(self._reduce_pixbuf)
+
+
+class InfoButton(gtk.EventBox):
+ def __init__(self):
+ super(InfoButton, self).__init__()
+
+ path = os.path.join(constants.GFX_PATH, 'corner-info.svg')
+ pixbuf = gdk.pixbuf_new_from_file(path)
+ self.width = pixbuf.get_width()
+ self.height = pixbuf.get_height()
+
+ self._image = gtk.image_new_from_pixbuf(pixbuf)
+ self._image.show()
+ self.add(self._image)
+
+
+class ImageBox(gtk.EventBox):
+ def __init__(self):
+ super(ImageBox, self).__init__()
+ self._pixbuf = None
+ self._image = gtk.Image()
+ self.add(self._image)
+
+ def show(self):
+ self._image.show()
+ super(ImageBox, self).show()
+
+ def hide(self):
+ self._image.hide()
+ super(ImageBox, self).hide()
+
+ def clear(self):
+ self._image.clear()
+ self._pixbuf = None
+
+ def set_pixbuf(self, pixbuf):
+ self._pixbuf = pixbuf
+
+ def set_size(self, width, height):
+ if self._pixbuf:
+ if width == self._pixbuf.get_width() and height == self._pixbuf.get_height():
+ pixbuf = self._pixbuf
+ else:
+ pixbuf = self._pixbuf.scale_simple(width, height, gdk.INTERP_BILINEAR)
+
+ self._image.set_from_pixbuf(pixbuf)
+
+ self._image.set_size_request(width, height)
+ self.set_size_request(width, height)
+
+class MediaView(gtk.EventBox):
+ """
+ A widget to show the main record UI with a video feed, but with some
+ extra features: possibility to show images, information UI about media,
+ etc.
+
+ It is made complicated because under the UI design, some widgets need
+ to be placed on top of others. In GTK+, this is not trivial. We achieve
+ this here by relying on the fact that GDK+ specifically allows us to
+ raise specific windows, and by packing all our Z-order-sensitive widgets
+ into EventBoxes (which have their own windows).
+ """
+ __gtype_name__ = "MediaView"
+ __gsignals__ = {
+ 'media-clicked': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+ 'pip-clicked': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+ 'full-clicked': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+ 'info-clicked': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+ 'tags-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_OBJECT,)),
+ }
+
+ MODE_LIVE = 0
+ MODE_PHOTO = 1
+ MODE_VIDEO = 2
+ MODE_STILL = 3
+ MODE_INFO_PHOTO = 4
+ MODE_INFO_VIDEO = 5
+
+ @staticmethod
+ def _raise_widget(widget):
+ widget.show()
+ widget.realize()
+ widget.window.raise_()
+
+ def __init__(self):
+ self._mode = None
+ self._allocation = None
+ self._hide_controls_timer = None
+
+ super(MediaView, self).__init__()
+ self.connect('size-allocate', self._size_allocate)
+ self.connect('motion-notify-event', self._motion_notify)
+ self.set_events(gdk.POINTER_MOTION_MASK | gdk.POINTER_MOTION_HINT_MASK)
+
+ self._fixed = gtk.Fixed()
+ self.add(self._fixed)
+
+ self._info_view = InfoView()
+ self._info_view.connect('primary-allocated', self._info_view_primary_allocated)
+ self._info_view.connect('secondary-allocated', self._info_view_secondary_allocated)
+ self._info_view.connect('tags-changed', self._info_view_tags_changed)
+ self._fixed.put(self._info_view, 0, 0)
+
+ self._image_box = ImageBox()
+ self._image_box.connect('button-release-event', self._image_clicked)
+ self._fixed.put(self._image_box, 0, 0)
+
+ self._video = VideoBox()
+ self._video.connect('button-release-event', self._video_clicked)
+ self._fixed.put(self._video, 0, 0)
+
+ self._video2 = VideoBox()
+ self._video2.connect('button-release-event', self._video2_clicked)
+ self._fixed.put(self._video2, 0, 0)
+
+ self._info_button = InfoButton()
+ self._info_button.connect('button-release-event', self._info_clicked)
+ self._fixed.put(self._info_button, 0, 0)
+
+ self._full_button = FullscreenButton()
+ self._full_button.connect('button-release-event', self._full_clicked)
+ self._fixed.put(self._full_button, 0, 0)
+
+ self._switch_mode(MediaView.MODE_LIVE)
+
+ def _size_allocate(self, widget, allocation):
+ # First check if we've already processed an allocation of this size.
+ # This is necessary because the operations we do in response to the
+ # size allocation cause another size allocation to happen.
+ if self._allocation == allocation:
+ return
+
+ self._allocation = allocation
+ self._place_widgets()
+
+ def _motion_notify(self, widget, event):
+ if self._hide_controls_timer:
+ # remove timer, it will be reprogrammed right after
+ gobject.source_remove(self._hide_controls_timer)
+ else:
+ self._show_controls()
+
+ self._hide_controls_timer = gobject.timeout_add(2000, self._hide_controls)
+
+ def _show_controls(self):
+ if self._mode in (MediaView.MODE_LIVE, MediaView.MODE_VIDEO, MediaView.MODE_PHOTO, MediaView.MODE_STILL):
+ self._raise_widget(self._full_button)
+
+ if self._mode in (MediaView.MODE_VIDEO, MediaView.MODE_PHOTO):
+ self._raise_widget(self._info_button)
+
+ if self._mode in (MediaView.MODE_VIDEO, MediaView.MODE_PHOTO):
+ self._raise_widget(self._video)
+
+ def _hide_controls(self):
+ if self._hide_controls_timer:
+ gobject.source_remove(self._hide_controls_timer)
+ self._hide_controls_timer = None
+
+ self._full_button.hide()
+ if self._mode not in (MediaView.MODE_INFO_PHOTO, MediaView.MODE_INFO_VIDEO):
+ self._info_button.hide()
+
+ if self._mode in (MediaView.MODE_VIDEO, MediaView.MODE_PHOTO):
+ self._video.hide()
+
+ return False
+
+ def _place_widgets(self):
+ allocation = self._allocation
+
+ self._image_box.hide()
+ self._video.hide()
+ self._video2.hide()
+ self._info_view.hide()
+ self._info_button.hide()
+
+ border = 5
+ full_button_x = allocation.width - border - self._full_button.width
+ full_button_y = border
+ self._fixed.move(self._full_button, full_button_x, full_button_y)
+
+ info_x = allocation.width - self._info_button.width
+ info_y = allocation.height - self._info_button.height
+ self._fixed.move(self._info_button, info_x, info_y)
+
+ if self._mode == MediaView.MODE_LIVE:
+ self._fixed.move(self._video, 0, 0)
+ self._video.set_size_request(allocation.width, allocation.height)
+ self._video.show()
+ self._image_box.clear()
+ elif self._mode == MediaView.MODE_VIDEO:
+ self._fixed.move(self._video2, 0, 0)
+ self._video2.set_size_request(allocation.width, allocation.height)
+ self._video2.show()
+ self._image_box.clear()
+
+ vid_h = allocation.height / 6
+ vid_w = allocation.width / 6
+ self._video.set_size_request(vid_w, vid_h)
+
+ border = 20
+ vid_x = border
+ vid_y = self.allocation.height - border - vid_h
+ self._fixed.move(self._video, vid_x, vid_y)
+ elif self._mode == MediaView.MODE_PHOTO:
+ self._fixed.move(self._image_box, 0, 0)
+ self._image_box.set_size(allocation.width, allocation.height)
+ self._image_box.show()
+
+ vid_h = allocation.height / 6
+ vid_w = allocation.width / 6
+ self._video.set_size_request(vid_w, vid_h)
+
+ border = 20
+ vid_x = border
+ vid_y = self.allocation.height - border - vid_h
+ self._fixed.move(self._video, vid_x, vid_y)
+ elif self._mode == MediaView.MODE_STILL:
+ self._fixed.move(self._image_box, 0, 0)
+ self._image_box.set_size(allocation.width, allocation.height)
+ self._image_box.show()
+ elif self._mode in (MediaView.MODE_INFO_PHOTO, MediaView.MODE_INFO_VIDEO):
+ self._full_button.hide()
+ self._info_view.set_size_request(allocation.width, allocation.height)
+ self._info_view.fit_to_allocation(allocation)
+ self._info_view.show()
+ self._raise_widget(self._info_button)
+
+ def _info_view_primary_allocated(self, widget, allocation):
+ if self._mode == MediaView.MODE_INFO_PHOTO:
+ self._fixed.move(self._image_box, allocation.x, allocation.y)
+ self._image_box.set_size(allocation.width, allocation.height)
+ self._raise_widget(self._image_box)
+ elif self._mode == MediaView.MODE_INFO_VIDEO:
+ self._fixed.move(self._video2, allocation.x, allocation.y)
+ self._video2.set_size_request(allocation.width, allocation.height)
+ self._raise_widget(self._video2)
+
+ def _info_view_secondary_allocated(self, widget, allocation):
+ if self._mode in (MediaView.MODE_INFO_PHOTO, MediaView.MODE_INFO_VIDEO):
+ self._fixed.move(self._video, allocation.x, allocation.y)
+ self._video.set_size_request(allocation.width, allocation.height)
+ self._raise_widget(self._video)
+
+ def _info_view_tags_changed(self, widget, tbuffer):
+ self.emit('tags-changed', tbuffer)
+
+ def _switch_mode(self, new_mode):
+ if self._mode == MediaView.MODE_LIVE and new_mode == MediaView.MODE_LIVE:
+ return
+ self._mode = new_mode
+
+ if self._hide_controls_timer:
+ gobject.source_remove(self._hide_controls_timer)
+ self._hide_controls_timer = None
+
+ if self._allocation:
+ self._place_widgets()
+
+ def _image_clicked(self, widget, event):
+ self.emit('media-clicked')
+
+ def _video_clicked(self, widget, event):
+ if self._mode != MediaView.MODE_LIVE:
+ self.emit('pip-clicked')
+
+ def _video2_clicked(self, widget, event):
+ self.emit('media-clicked')
+
+ def _full_clicked(self, widget, event):
+ self.emit('full-clicked')
+
+ def _info_clicked(self, widget, event):
+ self.emit('info-clicked')
+
+ def _show_info(self, mode, author, stroke, fill, date, tags):
+ self._info_view.set_author(author, stroke, fill)
+ self._info_view.set_date(date)
+ self._info_view.set_tags(tags)
+ self._switch_mode(mode)
+
+ def show_info_photo(self, author, stroke, fill, date, tags):
+ self._show_info(MediaView.MODE_INFO_PHOTO, author, stroke, fill, date, tags)
+
+ def show_info_video(self, author, stroke, fill, date, tags):
+ self._show_info(MediaView.MODE_INFO_VIDEO, author, stroke, fill, date, tags)
+
+ def set_fullscreen(self, fullscreen):
+ if self._hide_controls_timer:
+ gobject.source_remove(self._hide_controls_timer)
+ self._hide_controls_timer = None
+
+ if fullscreen:
+ self._full_button.set_reduce()
+ else:
+ self._full_button.set_enlarge()
+
+ def realize_video(self):
+ self._video.realize()
+ self._video2.realize()
+
+ # can be called from gstreamer thread, must not do any GTK+ stuff
+ def set_video_sink(self, sink):
+ self._video.set_sink(sink)
+
+ # can be called from gstreamer thread, must not do any GTK+ stuff
+ def set_video2_sink(self, sink):
+ self._video2.set_sink(sink)
+
+ def show_still(self, pixbuf):
+ # don't modify the original...
+ pixbuf = pixbuf.copy()
+ pixbuf.saturate_and_pixelate(pixbuf, 0, 0)
+ self._image_box.set_pixbuf(pixbuf)
+ self._switch_mode(MediaView.MODE_STILL)
+
+ def show_photo(self, path):
+ if path:
+ pixbuf = gdk.pixbuf_new_from_file(path)
+ self._image_box.set_pixbuf(pixbuf)
+ self._switch_mode(MediaView.MODE_PHOTO)
+
+ def show_video(self):
+ self._switch_mode(MediaView.MODE_VIDEO)
+
+ def show_live(self):
+ self._switch_mode(MediaView.MODE_LIVE)
+
+ def show(self):
+ self._fixed.show()
+ super(MediaView, self).show()
+
+ def hide(self):
+ self._fixed.hide()
+ super(MediaView, self).hide()
+
diff --git a/mediaview.pyc b/mediaview.pyc
new file mode 100644
index 0000000..c2a95dd
--- /dev/null
+++ b/mediaview.pyc
Binary files differ
diff --git a/model.py b/model.py
new file mode 100644
index 0000000..d2c5ade
--- /dev/null
+++ b/model.py
@@ -0,0 +1,403 @@
+# -*- coding: UTF-8 -*-
+#Copyright (c) 2008, Media Modifications Ltd.
+
+#Permission is hereby granted, free of charge, to any person obtaining a copy
+#of this software and associated documentation files (the "Software"), to deal
+#in the Software without restriction, including without limitation the rights
+#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+#copies of the Software, and to permit persons to whom the Software is
+#furnished to do so, subject to the following conditions:
+
+#The above copyright notice and this permission notice shall be included in
+#all copies or substantial portions of the Software.
+
+#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+#THE SOFTWARE.
+
+from gettext import gettext as _
+from xml.dom.minidom import parse
+import logging
+import uuid
+import os
+import time
+import json
+
+import gobject
+import gst
+
+import sugar.profile
+import sugar.env
+
+
+import constants
+from instance import Instance
+from recorded import Recorded
+import utils
+import serialize
+from collab import RecordCollab
+from glive import Glive
+from gplay import Gplay
+
+logger = logging.getLogger('model')
+
+class Model:
+ def __init__(self, activity_obj):
+ self.activity = activity_obj
+
+ self.collab = RecordCollab(self.activity, self)
+ self.glive = Glive(self.activity, self)
+ self.gplay = Gplay(self.activity)
+ self.gplay.connect('playback-status-changed', self._playback_status_changed)
+
+ self._mode = None
+ self._state = constants.STATE_READY
+ self._countdown_value = 0
+ self._countdown_handle = None
+ self._timer_value = 0
+ self._timer_duration = 0
+ self._timer_handle = None
+
+ self.mediaHashs = {}
+ for key, value in constants.MEDIA_INFO.items():
+ self.mediaHashs[key] = []
+
+ def write_file(self, path):
+ ui_serialized = self.activity.serialize()
+ self.mediaHashs['ui'] = ui_serialized
+ dom = serialize.saveMediaHash(self.mediaHashs, self.activity)
+ ui_data = json.dumps(ui_serialized)
+ ui_el = dom.createElement('ui')
+ ui_el.appendChild(dom.createTextNode(ui_data))
+ dom.documentElement.appendChild(ui_el)
+
+ fd = open(path, "w")
+ dom.writexml(fd)
+ fd.close()
+
+ def read_file(self, path):
+ try:
+ dom = parse(path)
+ except Exception, e:
+ logger.error('read_file: %s' % e)
+ return
+
+ serialize.fillMediaHash(dom, self.mediaHashs)
+ for i in dom.documentElement.getElementsByTagName('ui'):
+ for ui_el in i.childNodes:
+ self.activity.deserialize(json.loads(ui_el.data))
+
+ for recd in self.mediaHashs[self._mode]:
+ self.activity.add_thumbnail(recd, True)
+
+ def get_has_camera(self):
+ return self.glive.get_has_camera()
+
+ def get_nickname(self):
+ return sugar.profile.get_nick_name()
+
+ def get_mode(self):
+ return self._mode
+
+ def change_mode(self, mode):
+ if mode == self._mode:
+ return
+
+ self._mode = mode
+
+ self.activity.remove_all_thumbnails()
+ for recd in self.mediaHashs[mode]:
+ self.activity.add_thumbnail(recd, True)
+
+ #self.activity.set_mode(mode)
+ self.set_state(constants.STATE_READY)
+
+ if mode == constants.MODE_PHOTO:
+ self.glive.play()
+
+ def ui_frozen(self):
+ return not self._state == constants.STATE_READY
+
+ def set_state(self, state):
+ self._state = state
+
+ if state == constants.STATE_READY:
+ self.gplay.stop()
+
+ # if we aren't using Xv (e.g. glive is playing as PIP in video
+ # mode), then stop the pipeline so that we switch back to Xv
+ # in the call that follows.
+ if self.glive.get_has_camera() and not self.glive.is_using_xv():
+ self.glive.stop()
+
+ self.glive.play()
+
+ self.activity.set_state(state)
+
+ def get_state(self):
+ return self._state
+
+ def set_progress(self, value, text):
+ self.activity.set_progress(value, text)
+
+ def _timer_tick(self):
+ self._timer_value = self._timer_value - 1
+ value = self._timer_value
+ progress_value = 1 - (float(value) / float(self._timer_duration))
+
+ mins = value / 60
+ secs = value % 60
+ text = _('%d:%02d remaining') % (mins, secs)
+
+ self.set_progress(progress_value, text)
+
+ if self._timer_value <= 0:
+ self._timer_handle = None
+ self._timer_value = 0
+ self._stop_media_capture()
+ return False
+
+ return True
+
+ def _start_media_capture(self):
+ if self._mode == constants.MODE_PHOTO:
+ self.activity.set_shutter_sensitive(False)
+ self.glive.take_photo()
+ return
+
+ #self._timer_value = self.activity.get_selected_duration()
+ #self._timer_duration = self._timer_value
+ #self._timer_handle = gobject.timeout_add(1000, self._timer_tick)
+
+ #self.activity.set_shutter_sensitive(True)
+ #self.set_state(constants.STATE_RECORDING)
+
+ if self._mode == constants.MODE_VIDEO:
+ quality = self.activity.get_selected_quality()
+ self.glive.record_video(quality)
+ elif self._mode == constants.MODE_AUDIO:
+ self.glive.record_audio()
+
+ def _stop_media_capture(self):
+ if self._timer_handle:
+ gobject.source_remove(self._timer_handle)
+ self._timer_handle = None
+ self._timer_value = 0
+
+ self.set_progress(0, '')
+
+ if self._mode == constants.MODE_VIDEO:
+ self.glive.stop_recording_video()
+ elif self._mode == constants.MODE_AUDIO:
+ self.glive.stop_recording_audio()
+
+ self.set_state(constants.STATE_PROCESSING)
+
+
+
+ def _countdown_tick(self):
+ self._countdown_value = self._countdown_value - 1
+ value = self._countdown_value
+ self.activity.set_countdown(value)
+
+ if value <= 0:
+ self._countdown_handle = None
+ self._countdown_value = 0
+ self.shutter_sound(self._start_media_capture)
+ return False
+
+ return True
+
+ def do_shutter(self):
+ # if recording, stop
+ if self._state == constants.STATE_RECORDING:
+ self._stop_media_capture()
+ return
+
+ # if timer is selected, start countdown
+ #timer = self.activity.get_selected_timer()
+ #if timer > 0:
+ # self.activity.set_shutter_sensitive(False)
+ #self._countdown_value = self.activity.get_selected_timer()
+ # self._countdown_handle = gobject.timeout_add(1000, self._countdown_tick)
+ #return
+
+ # otherwise, capture normally
+ self._start_media_capture()
+
+ # called from gstreamer thread
+ def still_ready(self, pixbuf):
+ gobject.idle_add(self.activity.show_still, pixbuf)
+
+ def add_recd(self, recd):
+ self.mediaHashs[recd.type].append(recd)
+ if self._mode == recd.type:
+ self.activity.add_thumbnail(recd, True)
+
+ if not recd.buddy:
+ self.collab.share_recd(recd)
+
+ # called from gstreamer thread
+ def save_photo(self, pixbuf):
+ recd = self.createNewRecorded(constants.TYPE_PHOTO)
+
+ imgpath = os.path.join(Instance.instancePath, recd.mediaFilename)
+ pixbuf.save(imgpath, "jpeg")
+
+ pixbuf = utils.generate_thumbnail(pixbuf)
+ pixbuf.save(recd.make_thumb_path(), "png")
+
+ #now that we've saved both the image and its pixbuf, we get their md5s
+ self.createNewRecordedMd5Sums( recd )
+
+ gobject.idle_add(self.add_recd, recd, priority=gobject.PRIORITY_HIGH)
+ gobject.idle_add(self.activity.set_shutter_sensitive, True, priority=gobject.PRIORITY_HIGH)
+
+ # called from gstreamer thread
+ def save_video(self, path, still):
+ recd = self.createNewRecorded(constants.TYPE_VIDEO)
+ os.rename(path, os.path.join(Instance.instancePath, recd.mediaFilename))
+
+ still = utils.generate_thumbnail(still)
+ still.save(recd.make_thumb_path(), "png")
+
+ self.createNewRecordedMd5Sums( recd )
+
+ gobject.idle_add(self.add_recd, recd, priority=gobject.PRIORITY_HIGH)
+ gobject.idle_add(self.set_state, constants.STATE_READY)
+
+ def save_audio(self, path, still):
+ recd = self.createNewRecorded(constants.TYPE_AUDIO)
+ os.rename(path, os.path.join(Instance.instancePath, recd.mediaFilename))
+
+ if still:
+ image_path = os.path.join(Instance.instancePath, "audioPicture.png")
+ image_path = utils.getUniqueFilepath(image_path, 0)
+ still.save(image_path, "png")
+ recd.audioImageFilename = os.path.basename(image_path)
+
+ still = utils.generate_thumbnail(still)
+ still.save(recd.make_thumb_path(), "png")
+
+ self.createNewRecordedMd5Sums( recd )
+
+ gobject.idle_add(self.add_recd, recd, priority=gobject.PRIORITY_HIGH)
+ gobject.idle_add(self.set_state, constants.STATE_READY)
+
+ def _playback_status_changed(self, widget, status, value):
+ self.activity.set_playback_scale(value)
+ if status == gst.STATE_NULL:
+ self.activity.set_paused(True)
+
+ def play_audio(self, recd):
+ self.gplay.set_location("file://" + recd.getMediaFilepath())
+ self.gplay.play()
+ self.activity.set_paused(False)
+
+ def play_video(self, recd):
+ self.gplay.set_location("file://" + recd.getMediaFilepath())
+ self.glive.stop()
+ self.gplay.play()
+ self.glive.play(use_xv=False)
+ self.activity.set_paused(False)
+
+ def play_pause(self):
+ if self.gplay.get_state() == gst.STATE_PLAYING:
+ self.gplay.pause()
+ self.activity.set_paused(True)
+ else:
+ self.gplay.play()
+ self.activity.set_paused(False)
+
+ def start_seek(self):
+ self.gplay.pause()
+
+ def do_seek(self, position):
+ self.gplay.seek(position)
+
+ def end_seek(self):
+ self.gplay.play()
+
+ def get_recd_by_md5(self, md5):
+ for mh in self.mediaHashs.values():
+ for recd in mh:
+ if recd.thumbMd5 == md5 or recd.mediaMd5 == md5:
+ return recd
+
+ return None
+
+ def createNewRecorded(self, type):
+ recd = Recorded()
+
+ recd.recorderName = self.get_nickname()
+ recd.recorderHash = Instance.keyHashPrintable
+
+ #to create a file, use the hardware_id+time *and* check if available or not
+ nowtime = int(time.time())
+ recd.time = nowtime
+ recd.type = type
+
+ mediaThumbFilename = str(recd.recorderHash) + "_" + str(recd.time)
+ mediaFilename = mediaThumbFilename
+ mediaFilename = mediaFilename + "." + constants.MEDIA_INFO[type]['ext']
+ mediaFilepath = os.path.join( Instance.instancePath, mediaFilename )
+ mediaFilepath = utils.getUniqueFilepath( mediaFilepath, 0 )
+ recd.mediaFilename = os.path.basename( mediaFilepath )
+
+ stringType = constants.MEDIA_INFO[type]['istr']
+
+ # Translators: photo by photographer, e.g. "Photo by Mary"
+ recd.title = _('%s by %s') % (stringType, recd.recorderName)
+
+ color = sugar.profile.get_color()
+ recd.colorStroke = color.get_stroke_color()
+ recd.colorFill = color.get_fill_color()
+
+ logger.debug('createNewRecorded: ' + str(recd))
+ return recd
+
+ def createNewRecordedMd5Sums( self, recd ):
+ recd.thumbMd5 = recd.mediaMd5 = str(uuid.uuid4())
+
+ #load the thumbfile
+ if recd.thumbFilename:
+ thumbFile = os.path.join(Instance.instancePath, recd.thumbFilename)
+ recd.thumbBytes = os.stat(thumbFile)[6]
+
+ recd.tags = ""
+
+ #load the mediafile
+ mediaFile = os.path.join(Instance.instancePath, recd.mediaFilename)
+ mBytes = os.stat(mediaFile)[6]
+ recd.mediaBytes = mBytes
+
+ def delete_recd(self, recd):
+ recd.deleted = True
+ self.mediaHashs[recd.type].remove(recd)
+
+ if recd.meshUploading:
+ return
+
+ #remove files from the filesystem if not on the datastore
+ if recd.datastoreId == None:
+ mediaFile = recd.getMediaFilepath()
+ if os.path.exists(mediaFile):
+ os.remove(mediaFile)
+
+ thumbFile = recd.getThumbFilepath()
+ if thumbFile and os.path.exists(thumbFile):
+ os.remove(thumbFile)
+ else:
+ #remove from the datastore here, since once gone, it is gone...
+ serialize.removeMediaFromDatastore(recd)
+
+ def request_download(self, recd):
+ self.activity.show_still(recd.getThumbPixbuf())
+ self.set_state(constants.STATE_DOWNLOADING)
+ self.collab.request_download(recd)
+ self.activity.update_download_progress(recd)
+
diff --git a/model.pyc b/model.pyc
new file mode 100644
index 0000000..ecaf27d
--- /dev/null
+++ b/model.pyc
Binary files differ
diff --git a/record.py b/record.py
new file mode 100644
index 0000000..e6c7bb3
--- /dev/null
+++ b/record.py
@@ -0,0 +1,934 @@
+#Copyright (c) 2008, Media Modifications Ltd.
+
+#Permission is hereby granted, free of charge, to any person obtaining a copy
+#of this software and associated documentation files (the "Software"), to deal
+#in the Software without restriction, including without limitation the rights
+#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+#copies of the Software, and to permit persons to whom the Software is
+#furnished to do so, subject to the following conditions:
+
+#The above copyright notice and this permission notice shall be included in
+#all copies or substantial portions of the Software.
+
+#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+#THE SOFTWARE.
+
+import os
+import logging
+import shutil
+from gettext import gettext as _
+from gettext import ngettext
+
+import gtk
+from gtk import gdk
+import cairo
+import pango
+import pangocairo
+import pygst
+pygst.require('0.10')
+import gst
+
+from sugar.activity import activity
+from sugar.graphics.toolcombobox import ToolComboBox
+from sugar.graphics.toolbarbox import ToolbarBox
+from sugar.graphics.toolbarbox import ToolbarButton
+from sugar.graphics.radiotoolbutton import RadioToolButton
+from sugar.activity.widgets import StopButton
+from sugar.activity.widgets import ActivityToolbarButton
+
+from model import Model
+from button import RecdButton
+import constants
+from instance import Instance
+import utils
+from tray import HTray
+from mediaview import MediaView
+import hw
+from iconcombobox import IconComboBox
+
+logger = logging.getLogger('record.py')
+COLOR_BLACK = gdk.color_parse('#000000')
+COLOR_WHITE = gdk.color_parse('#ffffff')
+
+gst.debug_set_active(True)
+gst.debug_set_colored(False)
+if logging.getLogger().level <= logging.DEBUG:
+ gst.debug_set_default_threshold(gst.LEVEL_WARNING)
+else:
+ gst.debug_set_default_threshold(gst.LEVEL_ERROR)
+
+class Record(activity.Activity):
+ def __init__(self, handle):
+ super(Record, self).__init__(handle)
+ self.props.enable_fullscreen_mode = False
+ Instance(self)
+
+ #the main classes
+ self.model = Model(self)
+ self.ui_init()
+
+ #CSCL
+ self.connect("shared", self._shared_cb)
+ if self.get_shared_activity():
+ #have you joined or shared this activity yourself?
+ if self.get_shared():
+ self._joined_cb(self)
+ else:
+ self.connect("joined", self._joined_cb)
+
+ # Realize the video view widget so that it knows its own window XID
+ self._media_view.realize_video()
+
+ # Changing to the first toolbar kicks off the rest of the setup
+ if self.model.get_has_camera():
+ self.model.change_mode(constants.MODE_PHOTO)
+ else:
+ self.model.change_mode(constants.MODE_AUDIO)
+
+ def read_file(self, path):
+ self.model.read_file(path)
+
+ def write_file(self, path):
+ self.model.write_file(path)
+
+ def close(self):
+ self.model.gplay.stop()
+ self.model.glive.stop()
+ super(Record, self).close()
+
+ def _shared_cb(self, activity):
+ self.model.collab.set_activity_shared()
+
+ def _joined_cb(self, activity):
+ self.model.collab.joined()
+
+ def ui_init(self):
+ self._fullscreen = False
+ self._showing_info = False
+
+ # FIXME: if _thumb_tray becomes some kind of button group, we wouldn't
+ # have to track which recd is active
+ self._active_recd = None
+
+ self.connect_after('key-press-event', self._key_pressed)
+
+ self._active_toolbar_idx = 0
+
+ self._toolbar_box = ToolbarBox()
+ activity_button = ActivityToolbarButton(self)
+ self._toolbar_box.toolbar.insert(activity_button, 0)
+ self.set_toolbar_box(self._toolbar_box)
+ self._toolbar = self.get_toolbar_box().toolbar
+
+ tool_group = None
+ if self.model.get_has_camera():
+ self._photo_button = RadioToolButton()
+ self._photo_button.props.group = tool_group
+ tool_group = self._photo_button
+ self._photo_button.props.icon_name = 'camera-external'
+ self._photo_button.props.label = _('Photo')
+ self._photo_button.mode = constants.MODE_PHOTO
+ self._photo_button.connect('clicked', self._mode_button_clicked)
+ self._toolbar.insert(self._photo_button, -1)
+
+ self._video_button = RadioToolButton()
+ self._video_button.props.group = tool_group
+ self._video_button.props.icon_name = 'media-video'
+ self._video_button.props.label = _('Video')
+ self._video_button.mode = constants.MODE_VIDEO
+ self._video_button.connect('clicked', self._mode_button_clicked)
+ self._toolbar.insert(self._video_button, -1)
+ else:
+ self._photo_button = None
+ self._video_button = None
+
+ self._audio_button = RadioToolButton()
+ self._audio_button.props.group = tool_group
+ self._audio_button.props.icon_name = 'media-audio'
+ self._audio_button.props.label = _('Audio')
+ self._audio_button.mode = constants.MODE_AUDIO
+ self._audio_button.connect('clicked', self._mode_button_clicked)
+ self._toolbar.insert(self._audio_button, -1)
+
+ self._toolbar.insert(gtk.SeparatorToolItem(), -1)
+
+ self._toolbar_controls = RecordControl(self._toolbar)
+
+ separator = gtk.SeparatorToolItem()
+ separator.props.draw = False
+ separator.set_expand(True)
+ self._toolbar.insert(separator, -1)
+ self._toolbar.insert(StopButton(self), -1)
+ self.get_toolbar_box().show_all()
+
+ main_box = gtk.VBox()
+ self.set_canvas(main_box)
+ main_box.get_parent().modify_bg(gtk.STATE_NORMAL, COLOR_BLACK)
+ main_box.show()
+
+ self._media_view = MediaView()
+ self._media_view.connect('media-clicked', self._media_view_media_clicked)
+ self._media_view.connect('pip-clicked', self._media_view_pip_clicked)
+ self._media_view.connect('info-clicked', self._media_view_info_clicked)
+ self._media_view.connect('full-clicked', self._media_view_full_clicked)
+ self._media_view.connect('tags-changed', self._media_view_tags_changed)
+ self._media_view.show()
+
+ self._controls_hbox = gtk.HBox()
+ self._controls_hbox.show()
+
+ self._shutter_button = ShutterButton()
+ self._shutter_button.connect("clicked", self._shutter_clicked)
+ self._controls_hbox.pack_start(self._shutter_button, expand=True, fill=False)
+
+ self._countdown_image = CountdownImage()
+ self._controls_hbox.pack_start(self._countdown_image, expand=True, fill=False)
+
+ self._play_button = PlayButton()
+ self._play_button.connect('clicked', self._play_pause_clicked)
+ self._controls_hbox.pack_start(self._play_button, expand=False)
+
+ self._playback_scale = PlaybackScale(self.model)
+ self._controls_hbox.pack_start(self._playback_scale, expand=True, fill=True)
+
+ self._progress = ProgressInfo()
+ self._controls_hbox.pack_start(self._progress, expand=True, fill=True)
+
+ self._title_label = gtk.Label()
+ self._title_label.set_markup("<b><span foreground='white'>"+_('Title:')+'</span></b>')
+ self._controls_hbox.pack_start(self._title_label, expand=False)
+
+ self._title_entry = gtk.Entry()
+ self._title_entry.modify_bg(gtk.STATE_INSENSITIVE, COLOR_BLACK)
+ self._title_entry.connect('changed', self._title_changed)
+ self._controls_hbox.pack_start(self._title_entry, expand=True, fill=True, padding=10)
+
+ container = RecordContainer(self._media_view, self._controls_hbox)
+ main_box.pack_start(container, expand=True, fill=True, padding=6)
+ container.show()
+
+ self._thumb_tray = HTray()
+ self._thumb_tray.set_size_request(-1, 150)
+ main_box.pack_end(self._thumb_tray, expand=False)
+ self._thumb_tray.show_all()
+
+ def serialize(self):
+ data = {}
+
+ data['timer'] = self._toolbar_controls.get_timer_idx()
+ data['duration'] = self._toolbar_controls.get_duration_idx()
+ data['quality'] = self._toolbar_controls.get_quality()
+
+ return data
+
+ def deserialize(self, data):
+ self._toolbar_controls.set_timer_idx(data.get('timer', 0))
+ self._toolbar_controls.set_duration_idx(data.get('duration', 0))
+ self._toolbar_controls.set_quality(data.get('quality', 0))
+
+ def _key_pressed(self, widget, event):
+ if self.model.ui_frozen():
+ return False
+
+ key = event.keyval
+
+ if key == gtk.keysyms.KP_Page_Up: # game key O
+ if self._shutter_button.props.visible:
+ if self._shutter_button.props.sensitive:
+ self._shutter_button.clicked()
+ else: # return to live mode
+ self.model.set_state(constants.STATE_READY)
+ elif key == gtk.keysyms.c and event.state == gdk.CONTROL_MASK:
+ self._copy_to_clipboard(self._active_recd)
+ elif key == gtk.keysyms.i:
+ self._toggle_info()
+ elif key == gtk.keysyms.Escape:
+ if self._fullscreen:
+ self._toggle_fullscreen()
+
+ return False
+
+ def _play_pause_clicked(self, widget):
+ self.model.play_pause()
+
+ def set_mode(self, mode):
+ self._toolbar_controls.set_mode(mode)
+
+ # can be called from gstreamer thread, so must not do any GTK+ stuff
+ def set_glive_sink(self, sink):
+ return self._media_view.set_video_sink(sink)
+
+ # can be called from gstreamer thread, so must not do any GTK+ stuff
+ def set_gplay_sink(self, sink):
+ return self._media_view.set_video2_sink(sink)
+
+ def get_selected_quality(self):
+ return self._toolbar_controls.get_quality()
+
+ def get_selected_timer(self):
+ return self._toolbar_controls.get_timer()
+
+ def get_selected_duration(self):
+ return self._toolbar_controls.get_duration()
+
+ def set_progress(self, value, text):
+ self._progress.set_progress(value)
+ self._progress.set_text(text)
+
+ def set_countdown(self, value):
+ if value == 0:
+ self._shutter_button.show()
+ self._countdown_image.hide()
+ self._countdown_image.clear()
+ return
+
+ self._shutter_button.hide()
+ self._countdown_image.show()
+ self._countdown_image.set_value(value)
+
+ def _title_changed(self, widget):
+ self._active_recd.setTitle(self._title_entry.get_text())
+
+ def _media_view_media_clicked(self, widget):
+ if self._play_button.props.visible and self._play_button.props.sensitive:
+ self._play_button.clicked()
+
+ def _media_view_pip_clicked(self, widget):
+ # clicking on the PIP always returns to live mode
+ self.model.set_state(constants.STATE_READY)
+
+ def _media_view_info_clicked(self, widget):
+ self._toggle_info()
+
+ def _toggle_info(self):
+ recd = self._active_recd
+ if not recd:
+ return
+
+ if self._showing_info:
+ self._show_recd(recd, play=False)
+ return
+
+ self._showing_info = True
+ if self.model.get_mode() in (constants.MODE_PHOTO, constants.MODE_AUDIO):
+ func = self._media_view.show_info_photo
+ else:
+ func = self._media_view.show_info_video
+
+ self._play_button.hide()
+ self._progress.hide()
+ self._playback_scale.hide()
+ self._title_entry.set_text(recd.title)
+ self._title_entry.show()
+ self._title_label.show()
+
+ func(recd.recorderName, recd.colorStroke, recd.colorFill, utils.getDateString(recd.time), recd.tags)
+
+ def _media_view_full_clicked(self, widget):
+ self._toggle_fullscreen()
+
+ def _media_view_tags_changed(self, widget, tbuffer):
+ text = tbuffer.get_text(tbuffer.get_start_iter(), tbuffer.get_end_iter())
+ self._active_recd.setTags(text)
+
+ def _toggle_fullscreen(self):
+ if not self._fullscreen:
+ self._toolbar_box.hide()
+ self._thumb_tray.hide()
+ else:
+ self._toolbar_box.show()
+ self._thumb_tray.show()
+
+ self._fullscreen = not self._fullscreen
+ self._media_view.set_fullscreen(self._fullscreen)
+
+ def _mode_button_clicked(self, button):
+ self.model.change_mode(button.mode)
+
+ def _shutter_clicked(self, arg):
+ self.model.do_shutter()
+
+ def set_shutter_sensitive(self, value):
+ self._shutter_button.set_sensitive(value)
+
+ def set_state(self, state):
+ radio_state = (state == constants.STATE_READY)
+ for item in (self._photo_button, self._audio_button, self._video_button):
+ if item:
+ item.set_sensitive(radio_state)
+
+ self._showing_info = False
+ if state == constants.STATE_READY:
+ self._set_cursor_default()
+ self._active_recd = None
+ self._title_entry.hide()
+ self._title_label.hide()
+ self._play_button.hide()
+ self._playback_scale.hide()
+ self._progress.hide()
+ self._controls_hbox.set_child_packing(self._shutter_button, expand=True, fill=False, padding=0, pack_type=gtk.PACK_START)
+ self._shutter_button.set_normal()
+ self._shutter_button.show()
+ self._media_view.show_live()
+ elif state == constants.STATE_RECORDING:
+ self._shutter_button.set_recording()
+ self._controls_hbox.set_child_packing(self._shutter_button, expand=False, fill=False, padding=0, pack_type=gtk.PACK_START)
+ self._progress.show()
+ elif state == constants.STATE_PROCESSING:
+ self._set_cursor_busy()
+ self._shutter_button.hide()
+ self._progress.show()
+ elif state == constants.STATE_DOWNLOADING:
+ self._shutter_button.hide()
+ self._progress.show()
+
+ def set_paused(self, value):
+ if value:
+ self._play_button.set_play()
+ else:
+ self._play_button.set_pause()
+
+ def _thumbnail_clicked(self, button, recd):
+ if self.model.ui_frozen():
+ return
+
+ self._active_recd = recd
+ self._show_recd(recd)
+
+ def add_thumbnail(self, recd, scroll_to_end):
+ button = RecdButton(recd)
+ clicked_handler = button.connect("clicked", self._thumbnail_clicked, recd)
+ remove_handler = button.connect("remove-requested", self._remove_recd)
+ clipboard_handler = button.connect("copy-clipboard-requested", self._thumbnail_copy_clipboard)
+ button.set_data('handler-ids', (clicked_handler, remove_handler, clipboard_handler))
+ self._thumb_tray.add_item(button)
+ button.show()
+ if scroll_to_end:
+ self._thumb_tray.scroll_to_end()
+
+ def _copy_to_clipboard(self, recd):
+ if recd == None:
+ return
+ if not recd.isClipboardCopyable():
+ return
+
+ media_path = recd.getMediaFilepath()
+ tmp_path = utils.getUniqueFilepath(media_path, 0)
+ shutil.copyfile(media_path, tmp_path)
+ gtk.Clipboard().set_with_data([('text/uri-list', 0, 0)], self._clipboard_get, self._clipboard_clear, tmp_path)
+
+ def _clipboard_get(self, clipboard, selection_data, info, path):
+ selection_data.set("text/uri-list", 8, "file://" + path)
+
+ def _clipboard_clear(self, clipboard, path):
+ if os.path.exists(path):
+ os.unlink(path)
+
+ def _thumbnail_copy_clipboard(self, recdbutton):
+ self._copy_to_clipboard(recdbutton.get_recd())
+
+ def _remove_recd(self, recdbutton):
+ recd = recdbutton.get_recd()
+ self.model.delete_recd(recd)
+ if self._active_recd == recd:
+ self.model.set_state(constants.STATE_READY)
+
+ self._remove_thumbnail(recdbutton)
+
+ def _remove_thumbnail(self, recdbutton):
+ handlers = recdbutton.get_data('handler-ids')
+ for handler in handlers:
+ recdbutton.disconnect(handler)
+
+ self._thumb_tray.remove_item(recdbutton)
+ recdbutton.cleanup()
+
+ def remove_all_thumbnails(self):
+ for child in self._thumb_tray.get_children():
+ self._remove_thumbnail(child)
+
+ def show_still(self, pixbuf):
+ self._media_view.show_still(pixbuf)
+
+ def _show_photo(self, recd):
+ path = self._get_photo_path(recd)
+ self._media_view.show_photo(path)
+ self._title_entry.set_text(recd.title)
+ self._title_entry.show()
+ self._title_label.show()
+ self._shutter_button.hide()
+ self._progress.hide()
+
+ def _show_audio(self, recd, play):
+ self._progress.hide()
+ self._shutter_button.hide()
+ self._title_entry.hide()
+ self._title_label.hide()
+ self._play_button.show()
+ self._playback_scale.show()
+ path = recd.getAudioImageFilepath()
+ self._media_view.show_photo(path)
+ if play:
+ self.model.play_audio(recd)
+
+ def _show_video(self, recd, play):
+ self._progress.hide()
+ self._shutter_button.hide()
+ self._title_entry.hide()
+ self._title_label.hide()
+ self._play_button.show()
+ self._playback_scale.show()
+ self._media_view.show_video()
+ if play:
+ self.model.play_video(recd)
+
+ def set_playback_scale(self, value):
+ self._playback_scale.set_value(value)
+
+ def _get_photo_path(self, recd):
+ # FIXME should live (partially) in recd?
+
+ #downloading = self.ca.requestMeshDownload(recd)
+ #self.MESHING = downloading
+
+ if True: #not downloading:
+ #self.progressWindow.updateProgress(0, "")
+ return recd.getMediaFilepath()
+
+ #maybe it is not downloaded from the mesh yet...
+ #but we can show the low res thumb in the interim
+ return recd.getThumbFilepath()
+
+ def _show_recd(self, recd, play=True):
+ self._showing_info = False
+
+ if recd.buddy and not recd.downloadedFromBuddy:
+ self.model.request_download(recd)
+ elif recd.type == constants.TYPE_PHOTO:
+ self._show_photo(recd)
+ elif recd.type == constants.TYPE_AUDIO:
+ self._show_audio(recd, play)
+ elif recd.type == constants.TYPE_VIDEO:
+ self._show_video(recd, play)
+
+ def remote_recd_available(self, recd):
+ if recd == self._active_recd:
+ self._show_recd(recd)
+
+ def update_download_progress(self, recd):
+ if recd != self._active_recd:
+ return
+
+ if not recd.meshDownloading:
+ msg = _('Download failed.')
+ elif recd.meshDownloadingProgress:
+ msg = _('Downloading...')
+ else:
+ msg = _('Requesting...')
+
+ self.set_progress(recd.meshDownlodingPercent, msg)
+
+ def _set_cursor_busy(self):
+ self.window.set_cursor(gdk.Cursor(gdk.WATCH))
+
+ def _set_cursor_default(self):
+ self.window.set_cursor(None)
+
+class RecordContainer(gtk.Container):
+ """
+ A custom Container that contains a media view area, and a controls hbox.
+
+ The controls hbox is given the first height that it requests, locked in
+ for the duration of the widget.
+ The media view is given the remainder of the space, but is constrained to
+ a strict 4:3 ratio, therefore deducing its width.
+ The controls hbox is given the same width, and both elements are centered
+ horizontall.y
+ """
+ __gtype_name__ = 'RecordContainer'
+
+ def __init__(self, media_view, controls_hbox):
+ self._media_view = media_view
+ self._controls_hbox = controls_hbox
+ self._controls_hbox_height = 0
+ super(RecordContainer, self).__init__()
+
+ for widget in (self._media_view, self._controls_hbox):
+ if widget.flags() & gtk.REALIZED:
+ widget.set_parent_window(self.window)
+
+ widget.set_parent(self)
+
+ def do_realize(self):
+ self.set_flags(gtk.REALIZED)
+
+ self.window = gdk.Window(
+ self.get_parent_window(),
+ window_type=gdk.WINDOW_CHILD,
+ x=self.allocation.x,
+ y=self.allocation.y,
+ width=self.allocation.width,
+ height=self.allocation.height,
+ wclass=gdk.INPUT_OUTPUT,
+ colormap=self.get_colormap(),
+ event_mask=self.get_events() | gdk.VISIBILITY_NOTIFY_MASK | gdk.EXPOSURE_MASK)
+ self.window.set_user_data(self)
+
+ self.set_style(self.style.attach(self.window))
+
+ for widget in (self._media_view, self._controls_hbox):
+ widget.set_parent_window(self.window)
+ self.queue_resize()
+
+ # GTK+ contains on exit if remove is not implemented
+ def do_remove(self, widget):
+ pass
+
+ def do_size_request(self, req):
+ # always request 320x240 (as a minimum for video)
+ req.width = 320
+ req.height = 240
+
+ self._media_view.size_request()
+
+ w, h = self._controls_hbox.size_request()
+
+ # add on height requested by controls hbox
+ if self._controls_hbox_height == 0:
+ self._controls_hbox_height = h
+
+ req.height += self._controls_hbox_height
+
+ @staticmethod
+ def _constrain_4_3(width, height):
+ if (width % 4 == 0) and (height % 3 == 0) and ((width / 4) * 3) == height:
+ return width, height # nothing to do
+
+ ratio = 4.0 / 3.0
+ if ratio * height > width:
+ width = (width / 4) * 4
+ height = int(width / ratio)
+ else:
+ height = (height / 3) * 3
+ width = int(ratio * height)
+
+ return width, height
+
+ @staticmethod
+ def _center_in_plane(plane_size, size):
+ return (plane_size - size) / 2
+
+ def do_size_allocate(self, allocation):
+ self.allocation = allocation
+
+ # give the controls hbox the height that it requested
+ remaining_height = self.allocation.height - self._controls_hbox_height
+
+ # give the mediaview the rest, constrained to 4/3 and centered
+ media_view_width, media_view_height = self._constrain_4_3(self.allocation.width, remaining_height)
+ media_view_x = self._center_in_plane(self.allocation.width, media_view_width)
+ media_view_y = self._center_in_plane(remaining_height, media_view_height)
+
+ # send allocation to mediaview
+ alloc = gdk.Rectangle()
+ alloc.width = media_view_width
+ alloc.height = media_view_height
+ alloc.x = media_view_x
+ alloc.y = media_view_y
+ self._media_view.size_allocate(alloc)
+
+ # position hbox at the bottom of the window, with the requested height,
+ # and the same width as the media view
+ alloc = gdk.Rectangle()
+ alloc.x = media_view_x
+ alloc.y = self.allocation.height - self._controls_hbox_height
+ alloc.width = media_view_width
+ alloc.height = self._controls_hbox_height
+ self._controls_hbox.size_allocate(alloc)
+
+ if self.flags() & gtk.REALIZED:
+ self.window.move_resize(*allocation)
+
+ def do_forall(self, include_internals, callback, data):
+ for widget in (self._media_view, self._controls_hbox):
+ callback(widget, data)
+
+class PlaybackScale(gtk.HScale):
+ def __init__(self, model):
+ self.model = model
+ self._change_handler = None
+ self._playback_adjustment = gtk.Adjustment(0.0, 0.00, 100.0, 0.1, 1.0, 1.0)
+ super(PlaybackScale, self).__init__(self._playback_adjustment)
+
+ self.set_draw_value(False)
+ self.set_update_policy(gtk.UPDATE_CONTINUOUS)
+ self.connect('button-press-event', self._button_press)
+ self.connect('button-release-event', self._button_release)
+
+ def set_value(self, value):
+ self._playback_adjustment.set_value(value)
+
+ def _value_changed(self, scale):
+ self.model.do_seek(scale.get_value())
+
+ def _button_press(self, widget, event):
+ self.model.start_seek()
+ self._change_handler = self.connect('value-changed', self._value_changed)
+
+ def _button_release(self, widget, event):
+ self.disconnect(self._change_handler)
+ self._change_handler = None
+ self.model.end_seek()
+
+
+class ProgressInfo(gtk.VBox):
+ def __init__(self):
+ super(ProgressInfo, self).__init__()
+
+ self._progress_bar = gtk.ProgressBar()
+ self._progress_bar.modify_bg(gtk.STATE_NORMAL, COLOR_BLACK)
+ self._progress_bar.modify_bg(gtk.STATE_INSENSITIVE, COLOR_BLACK)
+ self.pack_start(self._progress_bar, expand=True, fill=True, padding=5)
+
+ self._label = gtk.Label()
+ self._label.modify_fg(gtk.STATE_NORMAL, COLOR_WHITE)
+ self.pack_start(self._label, expand=True, fill=True)
+
+ def show(self):
+ self._progress_bar.show()
+ self._label.show()
+ super(ProgressInfo, self).show()
+
+ def hide(self):
+ self._progress_bar.hide()
+ self._label.hide()
+ super(ProgressInfo, self).hide()
+
+ def set_progress(self, value):
+ self._progress_bar.set_fraction(value)
+
+ def set_text(self, text):
+ self._label.set_text(text)
+
+
+class CountdownImage(gtk.Image):
+ def __init__(self):
+ super(CountdownImage, self).__init__()
+ self._countdown_images = {}
+
+ def _generate_image(self, num):
+ w = 55
+ h = w
+ pixmap = gdk.Pixmap(self.get_window(), w, h, -1)
+ ctx = pixmap.cairo_create()
+ ctx.rectangle(0, 0, w, h)
+ ctx.set_source_rgb(0, 0, 0)
+ ctx.fill()
+
+ x = 0
+ y = 4
+ ctx.translate(x, y)
+ circle_path = os.path.join(constants.GFX_PATH, 'media-circle.png')
+ surface = cairo.ImageSurface.create_from_png(circle_path)
+ ctx.set_source_surface(surface, 0, 0)
+ ctx.paint()
+ ctx.translate(-x, -y)
+
+ ctx.set_source_rgb(255, 255, 255)
+ pctx = pangocairo.CairoContext(ctx)
+ play = pctx.create_layout()
+ font = pango.FontDescription("sans 30")
+ play.set_font_description(font)
+ play.set_text(str(num))
+ dim = play.get_pixel_extents()
+ ctx.translate(-dim[0][0], -dim[0][1])
+ xoff = (w - dim[0][2]) / 2
+ yoff = (h - dim[0][3]) / 2
+ ctx.translate(xoff, yoff)
+ ctx.translate(-3, 0)
+ pctx.show_layout(play)
+ return pixmap
+
+ def set_value(self, num):
+ if num not in self._countdown_images:
+ self._countdown_images[num] = self._generate_image(num)
+
+ self.set_from_pixmap(self._countdown_images[num], None)
+
+
+class ShutterButton(gtk.Button):
+ def __init__(self):
+ gtk.Button.__init__(self)
+ self.set_relief(gtk.RELIEF_NONE)
+ self.set_focus_on_click(False)
+ self.modify_bg(gtk.STATE_ACTIVE, COLOR_BLACK)
+
+ path = os.path.join(constants.GFX_PATH, 'media-record.png')
+ self._rec_image = gtk.image_new_from_file(path)
+
+ path = os.path.join(constants.GFX_PATH, 'media-record-red.png')
+ self._rec_red_image = gtk.image_new_from_file(path)
+
+ path = os.path.join(constants.GFX_PATH, 'media-insensitive.png')
+ self._insensitive_image = gtk.image_new_from_file(path)
+
+ self.set_normal()
+
+ def set_sensitive(self, sensitive):
+ if sensitive:
+ self.set_image(self._rec_image)
+ else:
+ self.set_image(self._insensitive_image)
+ super(ShutterButton, self).set_sensitive(sensitive)
+
+ def set_normal(self):
+ self.set_image(self._rec_image)
+
+ def set_recording(self):
+ self.set_image(self._rec_red_image)
+
+
+class PlayButton(gtk.Button):
+ def __init__(self):
+ super(PlayButton, self).__init__()
+ self.set_relief(gtk.RELIEF_NONE)
+ self.set_focus_on_click(False)
+ self.modify_bg(gtk.STATE_ACTIVE, COLOR_BLACK)
+
+ path = os.path.join(constants.GFX_PATH, 'media-play.png')
+ self._play_image = gtk.image_new_from_file(path)
+
+ path = os.path.join(constants.GFX_PATH, 'media-pause.png')
+ self._pause_image = gtk.image_new_from_file(path)
+
+ self.set_play()
+
+ def set_play(self):
+ self.set_image(self._play_image)
+
+ def set_pause(self):
+ self.set_image(self._pause_image)
+
+
+class RecordControl():
+
+ def __init__(self, toolbar):
+ self._timer_combo = TimerCombo()
+ toolbar.insert(self._timer_combo, -1)
+
+ self._duration_combo = DurationCombo()
+ toolbar.insert(self._duration_combo, -1)
+
+ preferences_toolbar = gtk.Toolbar()
+ combo = gtk.combo_box_new_text()
+ self.quality = ToolComboBox(combo=combo, label_text=_('Quality:'))
+ self.quality.combo.append_text(_('Low'))
+ if hw.get_xo_version() != 1:
+ # Disable High quality on XO-1. The system simply isn't beefy
+ # enough for recording to work well.
+ self.quality.combo.append_text(_('High'))
+ self.quality.combo.set_active(0)
+ self.quality.show_all()
+ preferences_toolbar.insert(self.quality, -1)
+
+ preferences_button = ToolbarButton()
+ preferences_button.set_page(preferences_toolbar)
+ preferences_button.props.icon_name = 'preferences-system'
+ preferences_button.props.label = _('Preferences')
+ toolbar.insert(preferences_button, -1)
+
+ def set_mode(self, mode):
+ if mode == constants.MODE_PHOTO:
+ self.quality.set_sensitive(True)
+ self._timer_combo.set_sensitive(True)
+ self._duration_combo.set_sensitive(False)
+ if mode == constants.MODE_VIDEO:
+ self.quality.set_sensitive(True)
+ self._timer_combo.set_sensitive(True)
+ self._duration_combo.set_sensitive(True)
+ if mode == constants.MODE_AUDIO:
+ self.quality.set_sensitive(False)
+ self._timer_combo.set_sensitive(True)
+ self._duration_combo.set_sensitive(True)
+
+ def get_timer(self):
+ return self._timer_combo.get_value()
+
+ def get_timer_idx(self):
+ return self._timer_combo.get_value_idx()
+
+ def set_timer_idx(self, idx):
+ self._timer_combo.set_value_idx(idx)
+
+ def get_duration(self):
+ return self._duration_combo.get_value()
+
+ def get_duration_idx(self):
+ return self._duration_combo.get_value_idx()
+
+ def set_duration_idx(self, idx):
+ return self._duration_combo.set_value_idx(idx)
+
+ def get_quality(self):
+ return self.quality.combo.get_active()
+
+ def set_quality(self, idx):
+ self.quality.combo.set_active(idx)
+
+
+class TimerCombo(IconComboBox):
+ TIMERS = (0, 5, 10)
+
+ def __init__(self):
+ super(TimerCombo, self).__init__('timer')
+
+ for i in self.TIMERS:
+ if i == 0:
+ self.append_item(i, _('Immediate'))
+ else:
+ string = TimerCombo._seconds_string(i)
+ self.append_item(i, string)
+ self.combo.set_active(0)
+
+ def get_value(self):
+ return TimerCombo.TIMERS[self.combo.get_active()]
+
+ def get_value_idx(self):
+ return self.combo.get_active()
+
+ def set_value_idx(self, idx):
+ self.combo.set_active(idx)
+
+ @staticmethod
+ def _seconds_string(x):
+ return ngettext('%s second', '%s seconds', x) % x
+
+
+class DurationCombo(IconComboBox):
+ DURATIONS = (2, 4, 6)
+
+ def __init__(self):
+ super(DurationCombo, self).__init__('duration')
+
+ for i in self.DURATIONS:
+ string = DurationCombo._minutes_string(i)
+ self.append_item(i, string)
+ self.combo.set_active(0)
+
+ def get_value(self):
+ return 60 * self.DURATIONS[self.combo.get_active()]
+
+ def get_value_idx(self):
+ return self.combo.get_active()
+
+ def set_value_idx(self, idx):
+ self.combo.set_active(idx)
+
+ @staticmethod
+ def _minutes_string(x):
+ return ngettext('%s minute', '%s minutes', x) % x
diff --git a/recorded.py b/recorded.py
new file mode 100644
index 0000000..9296742
--- /dev/null
+++ b/recorded.py
@@ -0,0 +1,180 @@
+#Copyright (c) 2008, Media Modifications Ltd.
+
+#Permission is hereby granted, free of charge, to any person obtaining a copy
+#of this software and associated documentation files (the "Software"), to deal
+#in the Software without restriction, including without limitation the rights
+#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+#copies of the Software, and to permit persons to whom the Software is
+#furnished to do so, subject to the following conditions:
+
+#The above copyright notice and this permission notice shall be included in
+#all copies or substantial portions of the Software.
+
+#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+#THE SOFTWARE.
+
+import os
+import gtk
+
+import constants
+from instance import Instance
+import utils
+import serialize
+
+class Recorded:
+ def __init__( self ):
+ self.type = -1
+ self.time = None
+ self.recorderName = None
+ self.recorderHash = None
+ self.title = None
+ self.colorStroke = None
+ self.colorFill = None
+ self.mediaMd5 = None
+ self.thumbMd5 = None
+ self.mediaBytes = None
+ self.thumbBytes = None
+ self.tags = None
+
+ #flag to alert need to re-datastore the title
+ self.metaChange = False
+
+ #when you are datastore-serialized, you get one of these ids...
+ self.datastoreId = None
+ self.datastoreOb = None
+
+ #if not from the datastore, then your media is here...
+ self.mediaFilename = None
+ self.thumbFilename = None
+ self.audioImageFilename = None
+
+ #for flagging when you are being saved to the datastore for the first time...
+ #and just because you have a datastore id, doesn't mean you're saved
+ self.savedMedia = False
+ self.savedXml = False
+
+ #assume you took the picture
+ self.buddy = False
+ self.downloadedFromBuddy = False
+ self.triedMeshBuddies = []
+ self.meshDownloading = False
+ self.meshDownloadingFrom = ""
+ self.meshDownloadingFromNick = ""
+ self.meshDownlodingPercent = 0.0
+ self.meshDownloadingProgress = False
+ #if someone is downloading this, then hold onto it
+ self.meshUploading = False
+ self.meshReqCallbackId = 0
+
+ self.deleted = False
+
+
+ def setTitle( self, newTitle ):
+ if self.title == newTitle:
+ return
+ self.title = newTitle
+ self.metaChange = True
+
+
+ def setTags( self, newTags ):
+ self.tags = newTags
+ self.metaChange = True
+
+
+ def isClipboardCopyable( self ):
+ copyme = True
+ if (self.buddy):
+ if (not self.downloadedFromBuddy):
+ return False
+ return copyme
+
+
+ #scenarios:
+ #launch, your new thumb -- Journal/session
+ #launch, your new media -- Journal/session
+ #launch, their new thumb -- Journal/session/buddy
+ #launch, their new media -- ([request->]) Journal/session/buddy
+ #relaunch, your old thumb -- metadataPixbuf on request (or save to Journal/session..?)
+ #relaunch, your old media -- datastoreObject->file (hold onto the datastore object, delete if deleted)
+ #relaunch, their old thumb -- metadataPixbuf on request (or save to Journal/session..?)
+ #relaunch, their old media -- datastoreObject->file (hold onto the datastore object, delete if deleted) | ([request->]) Journal/session/buddy
+
+ def getThumbPixbuf( self ):
+ thumbFilepath = self.getThumbFilepath()
+ if thumbFilepath and os.path.isfile(thumbFilepath):
+ return gtk.gdk.pixbuf_new_from_file(thumbFilepath)
+ else:
+ return None
+
+
+ def getThumbFilepath( self ):
+ if not self.thumbFilename:
+ return None
+ return os.path.join(Instance.instancePath, self.thumbFilename)
+
+ def make_thumb_path(self):
+ thumbFilename = self.mediaFilename + "_thumb.jpg"
+ thumbFilepath = os.path.join(Instance.instancePath, thumbFilename)
+ thumbFilepath = utils.getUniqueFilepath(thumbFilepath, 0)
+ self.thumbFilename = os.path.basename(thumbFilepath)
+ return self.getThumbFilepath()
+
+ def getAudioImagePixbuf( self ):
+ audioPixbuf = None
+
+ if self.audioImageFilename == None:
+ audioPixbuf = self.getThumbPixbuf()
+ else:
+ audioFilepath = self.getAudioImageFilepath()
+ if (audioFilepath != None):
+ audioPixbuf = gtk.gdk.pixbuf_new_from_file(audioFilepath)
+
+ return audioPixbuf
+
+
+ def getAudioImageFilepath( self ):
+ if (self.audioImageFilename != None):
+ audioFilepath = os.path.join(Instance.instancePath, self.audioImageFilename)
+ return os.path.abspath(audioFilepath)
+ else:
+ return self.getThumbFilepath()
+
+
+ def getMediaFilepath(self):
+ if (self.datastoreId == None):
+ if (not self.buddy):
+ #just taken by you, so it is in the tempSessionDir
+ mediaFilepath = os.path.join(Instance.instancePath, self.mediaFilename)
+ return os.path.abspath(mediaFilepath)
+ else:
+ if (self.downloadedFromBuddy):
+ #the user has requested the high-res version, and it has downloaded
+ mediaFilepath = os.path.join(Instance.instancePath, self.mediaFilename)
+ return os.path.abspath(mediaFilepath)
+ else:
+ if self.mediaFilename == None:
+ #creating a new filepath, probably just got here from the mesh
+ ext = constants.MEDIA_INFO[self.type]['ext']
+ recdPath = os.path.join(Instance.instancePath, "recdFile_"+self.mediaMd5+"."+ext)
+ recdPath = utils.getUniqueFilepath(recdPath, 0)
+ self.mediaFilename = os.path.basename(recdPath)
+ mediaFilepath = os.path.join(Instance.instancePath, self.mediaFilename)
+ return os.path.abspath(mediaFilepath)
+ else:
+ mediaFilepath = os.path.join(Instance.instancePath, self.mediaFilename)
+ return os.path.abspath(mediaFilepath)
+
+ else: #pulling from the datastore, regardless of who took it, cause we got it
+ #first, get the datastoreObject and hold the reference in this Recorded instance
+ if (self.datastoreOb == None):
+ self.datastoreOb = serialize.getMediaFromDatastore( self )
+ if (self.datastoreOb == None):
+ print("RecordActivity error -- unable to get datastore object in getMediaFilepath")
+ return None
+
+ return self.datastoreOb.file_path
diff --git a/recorded.pyc b/recorded.pyc
new file mode 100644
index 0000000..2372d36
--- /dev/null
+++ b/recorded.pyc
Binary files differ
diff --git a/serialize.py b/serialize.py
new file mode 100644
index 0000000..687bbaa
--- /dev/null
+++ b/serialize.py
@@ -0,0 +1,297 @@
+from xml.dom.minidom import getDOMImplementation
+import cStringIO
+import os
+import gtk
+import logging
+
+from sugar.datastore import datastore
+
+import constants
+from instance import Instance
+import recorded
+import utils
+
+logger = logging.getLogger('serialize')
+
+def fillMediaHash(doc, mediaHashs):
+ for key, value in constants.MEDIA_INFO.items():
+ recdElements = doc.documentElement.getElementsByTagName(value['name'])
+ for el in recdElements:
+ _loadMediaIntoHash( el, mediaHashs[key] )
+
+def _loadMediaIntoHash(el, hash):
+ addToHash = True
+ recd = recorded.Recorded()
+ recd = fillRecdFromNode(recd, el)
+ if recd:
+ if recd.datastoreId:
+ #quickly check: if you have a datastoreId that the file hasn't been deleted,
+ #cause if you do, we need to flag your removal
+ #2904 trac
+ recd.datastoreOb = getMediaFromDatastore( recd )
+ if not recd.datastoreOb:
+ addToHash = False
+ else:
+ #name might have been changed in the journal, so reflect that here
+ if recd.title != recd.datastoreOb.metadata['title']:
+ recd.setTitle(recd.datastoreOb.metadata['title'])
+ if recd.tags != recd.datastoreOb.metadata['tags']:
+ recd.setTags(recd.datastoreOb.metadata['tags'])
+ if recd.buddy:
+ recd.downloadedFromBuddy = True
+
+ recd.datastoreOb == None
+
+ if addToHash:
+ hash.append(recd )
+
+def getMediaFromDatastore(recd):
+ if not recd.datastoreId:
+ return None
+
+ if recd.datastoreOb:
+ #already have the object
+ return recd.datastoreOb
+
+ mediaObject = None
+ try:
+ mediaObject = datastore.get(recd.datastoreId)
+ finally:
+ return mediaObject
+
+def removeMediaFromDatastore(recd):
+ #before this method is called, the media are removed from the file
+ if not recd.datastoreId or not recd.datastoreOb:
+ return
+
+ try:
+ recd.datastoreOb.destroy()
+ datastore.delete(recd.datastoreId)
+
+ recd.datastoreId = None
+ recd.datastoreOb = None
+ finally:
+ #todo: add error message here
+ pass
+
+def fillRecdFromNode(recd, el):
+ if el.getAttributeNode('type'):
+ recd.type = int(el.getAttribute('type'))
+
+ if el.getAttributeNode('title'):
+ recd.title = el.getAttribute('title')
+
+ if el.getAttributeNode('time'):
+ recd.time = int(el.getAttribute('time'))
+
+ if el.getAttributeNode('photographer'):
+ recd.recorderName = el.getAttribute('photographer')
+
+ if el.getAttributeNode('tags'):
+ recd.tags = el.getAttribute('tags')
+ else:
+ recd.tags = ""
+
+ if el.getAttributeNode('recorderHash'):
+ recd.recorderHash = el.getAttribute('recorderHash')
+
+ if el.getAttributeNode('colorStroke'):
+ recd.colorStroke = el.getAttribute('colorStroke')
+
+ if el.getAttributeNode('colorFill'):
+ recd.colorFill = el.getAttribute('colorFill')
+
+ if el.getAttributeNode('buddy'):
+ recd.buddy = (el.getAttribute('buddy') == "True")
+
+ if el.getAttributeNode('mediaMd5'):
+ recd.mediaMd5 = el.getAttribute('mediaMd5')
+
+ if el.getAttributeNode('thumbMd5'):
+ recd.thumbMd5 = el.getAttribute('thumbMd5')
+
+ if el.getAttributeNode('mediaBytes'):
+ recd.mediaBytes = el.getAttribute('mediaBytes')
+
+ if el.getAttributeNode('thumbBytes'):
+ recd.thumbBytes = el.getAttribute('thumbBytes')
+
+ bt = el.getAttributeNode('base64Thumb')
+ if bt:
+ try:
+ thumbPath = os.path.join(Instance.instancePath, "datastoreThumb.jpg")
+ thumbPath = utils.getUniqueFilepath(thumbPath, 0)
+ thumbImg = utils.getPixbufFromString(bt.nodeValue)
+ thumbImg.save(thumbPath, "jpeg", {"quality":"85"} )
+ recd.thumbFilename = os.path.basename(thumbPath)
+ logger.debug("saved thumbFilename")
+ except:
+ logger.error("unable to getRecdBase64Thumb")
+
+ ai = el.getAttributeNode('audioImage')
+ if (not ai == None):
+ try:
+ audioImagePath = os.path.join(Instance.instancePath, "audioImage.png")
+ audioImagePath = utils.getUniqueFilepath( audioImagePath, 0 )
+ audioImage = utils.getPixbufFromString( ai.nodeValue )
+ audioImage.save(audioImagePath, "png", {} )
+ recd.audioImageFilename = os.path.basename(audioImagePath)
+ logger.debug("loaded audio image and set audioImageFilename")
+ except:
+ logger.error("unable to load audio image")
+
+ datastoreNode = el.getAttributeNode('datastoreId')
+ if datastoreNode:
+ recd.datastoreId = datastoreNode.nodeValue
+
+ return recd
+
+
+def getRecdXmlMeshString(recd):
+ impl = getDOMImplementation()
+ recdXml = impl.createDocument(None, 'recd', None)
+ root = recdXml.documentElement
+ _addRecdXmlAttrs(root, recd, True)
+
+ writer = cStringIO.StringIO()
+ recdXml.writexml(writer)
+ return writer.getvalue()
+
+def _addRecdXmlAttrs(el, recd, forMeshTransmit):
+ el.setAttribute('type', str(recd.type))
+
+ if (recd.type == constants.TYPE_AUDIO) and (not forMeshTransmit):
+ aiPixbuf = recd.getAudioImagePixbuf()
+ if aiPixbuf:
+ aiPixbufString = str(utils.getStringFromPixbuf(aiPixbuf))
+ el.setAttribute('audioImage', aiPixbufString)
+
+ if (recd.datastoreId != None) and (not forMeshTransmit):
+ el.setAttribute('datastoreId', str(recd.datastoreId))
+
+ el.setAttribute('title', recd.title)
+ el.setAttribute('time', str(recd.time))
+ el.setAttribute('photographer', recd.recorderName)
+ el.setAttribute('recorderHash', str(recd.recorderHash) )
+ el.setAttribute('colorStroke', str(recd.colorStroke) )
+ el.setAttribute('colorFill', str(recd.colorFill) )
+ el.setAttribute('buddy', str(recd.buddy))
+ el.setAttribute('mediaMd5', str(recd.mediaMd5))
+ el.setAttribute('thumbMd5', str(recd.thumbMd5))
+ el.setAttribute('mediaBytes', str(recd.mediaBytes))
+
+ if recd.thumbBytes:
+ el.setAttribute('thumbBytes', str(recd.thumbBytes))
+
+ # FIXME: can this be removed, or at least autodetected? has not been
+ # changed for ages, should not be relevant
+ el.setAttribute('version', '54')
+
+ pixbuf = recd.getThumbPixbuf()
+ if pixbuf:
+ thumb64 = str(utils.getStringFromPixbuf(pixbuf))
+ el.setAttribute('base64Thumb', thumb64)
+
+def saveMediaHash(mediaHashs, activity):
+ impl = getDOMImplementation()
+ album = impl.createDocument(None, 'album', None)
+ root = album.documentElement
+
+ #flag everything for saving...
+ atLeastOne = False
+ for type, value in constants.MEDIA_INFO.items():
+ typeName = value['name']
+ for recd in mediaHashs[type]:
+ recd.savedXml = False
+ recd.savedMedia = False
+ atLeastOne = True
+
+ #and if there is anything to save, save it
+ if atLeastOne:
+ for type, value in constants.MEDIA_INFO.items():
+ typeName = value['name']
+ for recd in mediaHashs[type]:
+ mediaEl = album.createElement(typeName)
+ root.appendChild(mediaEl)
+ _saveMedia(mediaEl, recd, activity)
+
+ return album
+
+def _saveMedia(el, recd, activity):
+ if recd.buddy == True and recd.datastoreId == None and not recd.downloadedFromBuddy:
+ recd.savedMedia = True
+ _saveXml(el, recd)
+ else:
+ recd.savedMedia = False
+ _saveMediaToDatastore(el, recd, activity)
+
+def _saveXml(el, recd):
+ _addRecdXmlAttrs(el, recd, False)
+ recd.savedXml = True
+
+def _saveMediaToDatastore(el, recd, activity):
+ #note that we update the recds that go through here to how they would
+ #look on a fresh load from file since this won't just happen on close()
+
+ if recd.datastoreId:
+ #already saved to the datastore, don't need to re-rewrite the file since the mediums are immutable
+ #However, they might have changed the name of the file
+ if recd.metaChange:
+ recd.datastoreOb = getMediaFromDatastore(recd)
+ if recd.datastoreOb.metadata['title'] != recd.title:
+ recd.datastoreOb.metadata['title'] = recd.title
+ datastore.write(recd.datastoreOb)
+ if recd.datastoreOb.metadata['tags'] != recd.tags:
+ recd.datastoreOb.metadata['tags'] = recd.tags
+ datastore.write(recd.datastoreOb)
+
+ #reset for the next title change if not closing...
+ recd.metaChange = False
+
+ #save the title to the xml
+ recd.savedMedia = True
+ _saveXml(el, recd)
+
+ else:
+ #this will remove the media from being accessed on the local disk since it puts it away into cold storage
+ #therefore this is only called when write_file is called by the activity superclass
+ mediaObject = datastore.create()
+ mediaObject.metadata['title'] = recd.title
+ mediaObject.metadata['tags'] = recd.tags
+
+ datastorePreviewPixbuf = recd.getThumbPixbuf()
+ if recd.type == constants.TYPE_AUDIO:
+ datastorePreviewPixbuf = recd.getAudioImagePixbuf()
+ elif recd.type == constants.TYPE_PHOTO:
+ datastorePreviewFilepath = recd.getMediaFilepath()
+ datastorePreviewPixbuf = gtk.gdk.pixbuf_new_from_file(datastorePreviewFilepath)
+
+ if datastorePreviewPixbuf:
+ datastorePreviewWidth = 300
+ datastorePreviewHeight = 225
+ if datastorePreviewPixbuf.get_width() != datastorePreviewWidth:
+ datastorePreviewPixbuf = datastorePreviewPixbuf.scale_simple(datastorePreviewWidth, datastorePreviewHeight, gtk.gdk.INTERP_NEAREST)
+
+ datastorePreviewBase64 = utils.getStringFromPixbuf(datastorePreviewPixbuf)
+ mediaObject.metadata['preview'] = datastorePreviewBase64
+
+ colors = str(recd.colorStroke) + "," + str(recd.colorFill)
+ mediaObject.metadata['icon-color'] = colors
+
+ mtype = constants.MEDIA_INFO[recd.type]
+ mediaObject.metadata['mime_type'] = mtype['mime']
+
+ mediaObject.metadata['activity_id'] = activity._activity_id
+
+ mediaFile = recd.getMediaFilepath()
+ mediaObject.file_path = mediaFile
+ mediaObject.transfer_ownership = True
+
+ datastore.write(mediaObject)
+
+ recd.datastoreId = mediaObject.object_id
+ recd.savedMedia = True
+
+ _saveXml(el, recd)
+
+ recd.mediaFilename = None
diff --git a/serialize.pyc b/serialize.pyc
new file mode 100644
index 0000000..a8b547d
--- /dev/null
+++ b/serialize.pyc
Binary files differ
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..6e4c8b1
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,12 @@
+#!/usr/bin/env python
+try:
+ from sugar.activity import bundlebuilder
+ bundlebuilder.start("ImageQuiz")
+except ImportError:
+ import os
+ os.system("find ./ | sed 's,^./,gtktest.activity/,g' > MANIFEST")
+ os.system('rm gtktest.xo')
+ os.chdir('..')
+ os.system('zip -r gtktest.xo gtktest.activity')
+ os.system('mv gtktest.xo ./gtktest.activity')
+ os.chdir('gtktest.activity') \ No newline at end of file
diff --git a/tray.py b/tray.py
new file mode 100644
index 0000000..4f7956d
--- /dev/null
+++ b/tray.py
@@ -0,0 +1,202 @@
+# Copyright (C) 2007, One Laptop Per Child
+#
+# 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 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.
+
+import gobject
+import gtk
+import hippo
+
+import sugar
+from sugar.graphics import style
+from sugar.graphics.icon import Icon
+
+_PREVIOUS_PAGE = 0
+_NEXT_PAGE = 1
+
+class _TrayViewport(gtk.Viewport):
+ __gproperties__ = {
+ 'can-scroll' : (bool, None, None, False,
+ gobject.PARAM_READABLE),
+ }
+
+ def __init__(self, orientation):
+ self.orientation = orientation
+ self._can_scroll = False
+
+ gobject.GObject.__init__(self)
+
+ self.set_shadow_type(gtk.SHADOW_NONE)
+
+ self.traybar = gtk.Toolbar()
+ self.traybar.set_orientation(orientation)
+ self.traybar.set_show_arrow(False)
+ self.add(self.traybar)
+ self.traybar.show()
+
+ self.connect('size_allocate', self._size_allocate_cb)
+
+ def scroll(self, direction):
+ if direction == _PREVIOUS_PAGE:
+ self._scroll_previous()
+ elif direction == _NEXT_PAGE:
+ self._scroll_next()
+
+ def _scroll_next(self):
+ if self.orientation == gtk.ORIENTATION_HORIZONTAL:
+ adj = self.get_hadjustment()
+ new_value = adj.value + self.allocation.width
+ adj.value = min(new_value, adj.upper - self.allocation.width)
+ else:
+ adj = self.get_vadjustment()
+ new_value = adj.value + self.allocation.height
+ adj.value = min(new_value, adj.upper - self.allocation.height)
+
+ def _scroll_to_end(self):
+ if self.orientation == gtk.ORIENTATION_HORIZONTAL:
+ adj = self.get_hadjustment()
+ adj.value = adj.upper# - self.allocation.width
+ else:
+ adj = self.get_vadjustment()
+ adj.value = adj.upper - self.allocation.height
+
+ def _scroll_previous(self):
+ if self.orientation == gtk.ORIENTATION_HORIZONTAL:
+ adj = self.get_hadjustment()
+ new_value = adj.value - self.allocation.width
+ adj.value = max(adj.lower, new_value)
+ else:
+ adj = self.get_vadjustment()
+ new_value = adj.value - self.allocation.height
+ adj.value = max(adj.lower, new_value)
+
+ def do_size_request(self, requisition):
+ child_requisition = self.child.size_request()
+ if self.orientation == gtk.ORIENTATION_HORIZONTAL:
+ requisition[0] = 0
+ requisition[1] = child_requisition[1]
+ else:
+ requisition[0] = child_requisition[0]
+ requisition[1] = 0
+
+ def do_get_property(self, pspec):
+ if pspec.name == 'can-scroll':
+ return self._can_scroll
+
+ def _size_allocate_cb(self, viewport, allocation):
+ bar_requisition = self.traybar.get_child_requisition()
+ if self.orientation == gtk.ORIENTATION_HORIZONTAL:
+ can_scroll = bar_requisition[0] > allocation.width
+ else:
+ can_scroll = bar_requisition[1] > allocation.height
+
+ if can_scroll != self._can_scroll:
+ self._can_scroll = can_scroll
+ self.notify('can-scroll')
+
+class _TrayScrollButton(gtk.Button):
+ def __init__(self, icon_name, scroll_direction):
+ gobject.GObject.__init__(self)
+
+ self._viewport = None
+
+ self._scroll_direction = scroll_direction
+
+ self.set_relief(gtk.RELIEF_NONE)
+ self.set_size_request(style.GRID_CELL_SIZE, style.GRID_CELL_SIZE)
+
+ icon = Icon(icon_name = icon_name,
+ icon_size=gtk.ICON_SIZE_SMALL_TOOLBAR)
+ self.set_image(icon)
+ icon.show()
+
+ self.connect('clicked', self._clicked_cb)
+
+ def set_viewport(self, viewport):
+ self._viewport = viewport
+ self._viewport.connect('notify::can-scroll',
+ self._viewport_can_scroll_changed_cb)
+
+ def _viewport_can_scroll_changed_cb(self, viewport, pspec):
+ #self.props.visible = self._viewport.props.can_scroll
+ self.set_sensitive(self._viewport.props.can_scroll)
+
+ def _clicked_cb(self, button):
+ self._viewport.scroll(self._scroll_direction)
+
+ viewport = property(fset=set_viewport)
+
+class HTray(gtk.VBox):
+ def __init__(self, **kwargs):
+ gobject.GObject.__init__(self, **kwargs)
+
+ separator = hippo.Canvas()
+ box = hippo.CanvasBox(
+ border_color=0xffffffff,
+ background_color=0xffffffff,
+ box_height=1,
+ border_bottom=1)
+ separator.set_root(box)
+ self.pack_start(separator, False)
+
+ hbox = gtk.HBox()
+ self.pack_start(hbox)
+
+ scroll_left = _TrayScrollButton('go-left', _PREVIOUS_PAGE)
+ scroll_left_event = gtk.EventBox()
+ scroll_left_event.add(scroll_left)
+ scroll_left_event.set_size_request(55, -1)
+ hbox.pack_start(scroll_left_event, False)
+
+ self._viewport = _TrayViewport(gtk.ORIENTATION_HORIZONTAL)
+ hbox.pack_start(self._viewport)
+ self._viewport.show()
+
+ scroll_right = _TrayScrollButton('go-right', _NEXT_PAGE)
+ scroll_right_event = gtk.EventBox()
+ scroll_right_event.add(scroll_right)
+ scroll_right_event.set_size_request(55, -1)
+ hbox.pack_start(scroll_right_event, False)
+
+ scroll_left.set_focus_on_click(False)
+ scroll_left_event.modify_bg(gtk.STATE_NORMAL, sugar.graphics.style.COLOR_TOOLBAR_GREY.get_gdk_color())
+ scroll_left.modify_bg(gtk.STATE_ACTIVE, sugar.graphics.style.COLOR_BUTTON_GREY.get_gdk_color())
+
+ scroll_right.set_focus_on_click(False)
+ scroll_right_event.modify_bg(gtk.STATE_NORMAL, sugar.graphics.style.COLOR_TOOLBAR_GREY.get_gdk_color())
+ scroll_right.modify_bg(gtk.STATE_ACTIVE, sugar.graphics.style.COLOR_BUTTON_GREY.get_gdk_color())
+
+ scroll_left.viewport = self._viewport
+ scroll_right.viewport = self._viewport
+
+ self.connect_after("size-allocate", self._sizeAllocateCb)
+
+ def _sizeAllocateCb(self, widget, event ):
+ self._viewport.notify('can-scroll')
+
+ def get_children(self):
+ return self._viewport.traybar.get_children()
+
+ def add_item(self, item, index=-1):
+ self._viewport.traybar.insert(item, index)
+
+ def remove_item(self, item):
+ self._viewport.traybar.remove(item)
+
+ def get_item_index(self, item):
+ return self._viewport.traybar.get_item_index(item)
+
+ def scroll_to_end(self):
+ self._viewport._scroll_to_end()
diff --git a/tray.pyc b/tray.pyc
new file mode 100644
index 0000000..40aa96e
--- /dev/null
+++ b/tray.pyc
Binary files differ
diff --git a/treeview.py b/treeview.py
new file mode 100644
index 0000000..2a8da2b
--- /dev/null
+++ b/treeview.py
@@ -0,0 +1,55 @@
+# [SNIPPET_NAME: Basic Tree View]
+# [SNIPPET_CATEGORIES: PyGTK]
+# [SNIPPET_DESCRIPTION: Create a basic tree view]
+# [SNIPPET_DOCS: http://www.pygtk.org/docs/pygtk/class-gtktreeview.html, http://www.pygtk.org/pygtk2tutorial/ch-TreeViewWidget.html]
+
+# example basictreeview.py
+
+import pygtk
+pygtk.require('2.0')
+import gtk
+
+class TreeView(gtk.TreeView):
+
+ def __init__(self):
+
+ gtk.TreeView.__init__(self)
+
+ # create a TreeStore with one string column to use as the model
+ self.treestore = gtk.TreeStore(str)
+
+ # we'll add some data now - 4 rows with 3 child rows each
+ for parent in range(4):
+ piter = self.treestore.append(None, ['parent %i' % parent])
+ for child in range(3):
+ self.treestore.append(piter, ['child %i of parent %i' %
+ (child, parent)])
+
+ # create the TreeView using treestore
+ self.set_model(self.treestore)
+
+ # create the TreeViewColumn to display the data
+ self.tvcolumn = gtk.TreeViewColumn('Column 0')
+
+ # add tvcolumn to treeview
+ self.append_column(self.tvcolumn)
+
+ # create a CellRendererText to render the data
+ self.cell = gtk.CellRendererText()
+
+ # add the cell to the tvcolumn and allow it to expand
+ self.tvcolumn.pack_start(self.cell, True)
+
+ # set the cell "text" attribute to column 0 - retrieve text
+ # from that column in treestore
+ self.tvcolumn.add_attribute(self.cell, 'text', 0)
+
+ # make it searchable
+ self.set_search_column(0)
+
+ # Allow sorting on the column
+ self.tvcolumn.set_sort_column_id(0)
+
+ # Allow drag and drop reordering of rows
+ self.set_reorderable(True)
+ self.show_all()
diff --git a/treeview.pyc b/treeview.pyc
new file mode 100644
index 0000000..78c9173
--- /dev/null
+++ b/treeview.pyc
Binary files differ
diff --git a/utils.py b/utils.py
new file mode 100644
index 0000000..28adc9c
--- /dev/null
+++ b/utils.py
@@ -0,0 +1,56 @@
+import base64
+import rsvg
+import re
+import os
+import gtk
+import time
+from time import strftime
+
+import constants
+
+def getStringFromPixbuf(pixbuf):
+ data = [""]
+ pixbuf.save_to_callback(_saveDataToBufferCb, "png", {}, data)
+ return base64.b64encode(str(data[0]))
+
+
+def _saveDataToBufferCb(buf, data):
+ data[0] += buf
+ return True
+
+
+def getPixbufFromString(str):
+ pbl = gtk.gdk.PixbufLoader()
+ data = base64.b64decode( str )
+ pbl.write(data)
+ pbl.close()
+ return pbl.get_pixbuf()
+
+
+def load_colored_svg(filename, stroke, fill):
+ path = os.path.join(constants.GFX_PATH, filename)
+ data = open(path, 'r').read()
+
+ entity = '<!ENTITY fill_color "%s">' % fill
+ data = re.sub('<!ENTITY fill_color .*>', entity, data)
+
+ entity = '<!ENTITY stroke_color "%s">' % stroke
+ data = re.sub('<!ENTITY stroke_color .*>', entity, data)
+
+ return rsvg.Handle(data=data).get_pixbuf()
+
+def getUniqueFilepath( path, i ):
+ pathOb = os.path.abspath( path )
+ newPath = os.path.join( os.path.dirname(pathOb), str( str(i) + os.path.basename(pathOb) ) )
+ if (os.path.exists(newPath)):
+ i = i + 1
+ return getUniqueFilepath( pathOb, i )
+ else:
+ return os.path.abspath( newPath )
+
+def generate_thumbnail(pixbuf):
+ return pixbuf.scale_simple(108, 81, gtk.gdk.INTERP_BILINEAR)
+
+def getDateString( when ):
+ return strftime( "%c", time.localtime(when) )
+
diff --git a/utils.pyc b/utils.pyc
new file mode 100644
index 0000000..86e5abb
--- /dev/null
+++ b/utils.pyc
Binary files differ