From c8480ef07eec6c3df4935ad3e02834eaf2d24b45 Mon Sep 17 00:00:00 2001 From: Cruz Antonio Cardenas Gomez Date: Fri, 30 Mar 2012 17:22:39 +0000 Subject: second commit --- 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(""+('Title:')+'') + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + +]> + + + + + + + + + + + \ 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 @@ + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + 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 @@ + + +]> + + + + + + \ 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 @@ + + +]> + + + + \ 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 @@ + + +]> + + + + + + + + + + \ 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 @@ + + + + + + +]> + + + + 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 @@ + + + + + + + en + Python + Qt4 + + 0.1 + + + + activity.py + model.py + tray.py + setup.py + serialize.py + recorded.py + mediaview.py + treeview.py + glive.py + constants.py + collab.py + utils.py + record.py + gplay.py + button.py + instance.py + __init__.py + + + + + + + + + + + + activity.py + + None + + + + + + + + + + + + + \ 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('' + _('Author:') + '') + 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('' + _('Tags:') + '') + 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('' + _('Date:') + ' ' + 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(""+_('Title:')+'') + 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 = '' % fill + data = re.sub('', entity, data) + + entity = '' % stroke + data = re.sub('', 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 -- cgit v0.9.1