diff options
author | Daniel Drake <dsd@laptop.org> | 2011-02-02 16:27:46 (GMT) |
---|---|---|
committer | Daniel Drake <dsd@laptop.org> | 2011-02-02 16:37:48 (GMT) |
commit | 3bc80c776f7ef2578a4b9fd26028574a12982ea3 (patch) | |
tree | 890aa0ee4a2b07d9c9704883d11f0385e03a2e70 | |
parent | ca2bbd6d74342247de97cffa150b577246c63107 (diff) |
UI rework
This is a rework of the UI that uses a more standard GTK+ principles
than previously. There is still a small amount of black magic, kept
away inside mediaview.py.
The UI/model separation was also refined in places, and a lot of code
was simplified.
Overall the functionality remains identical, and I tried to keep the UI
the same as before (but it is not pixel perfect).
As this was quite big there may be some bugs to shake out.
-rw-r--r-- | aplay.py | 6 | ||||
-rw-r--r-- | button.py | 140 | ||||
-rw-r--r-- | collab.py | 339 | ||||
-rw-r--r-- | color.py | 71 | ||||
-rw-r--r-- | constants.py | 389 | ||||
-rw-r--r-- | gfx/full-insensitive.png | bin | 813 -> 0 bytes | |||
-rw-r--r-- | gfx/info-on.svg | 17 | ||||
-rw-r--r-- | gfx/media-record.svg | 6 | ||||
-rw-r--r-- | glive.py | 713 | ||||
-rw-r--r-- | gplay.py | 141 | ||||
-rw-r--r-- | greplay.py | 80 | ||||
-rw-r--r-- | instance.py | 23 | ||||
-rw-r--r-- | mediaview.py | 512 | ||||
-rw-r--r-- | model.py | 558 | ||||
-rw-r--r-- | network.py | 1076 | ||||
-rw-r--r-- | p5.py | 190 | ||||
-rw-r--r-- | p5_button.py | 173 | ||||
-rw-r--r-- | port/AUTHORS | 1 | ||||
-rw-r--r-- | port/COPYING | 340 | ||||
-rw-r--r-- | port/NEWS | 8 | ||||
-rw-r--r-- | port/README | 13 | ||||
-rw-r--r-- | port/TODO | 0 | ||||
-rw-r--r-- | port/__init__.py | 0 | ||||
-rw-r--r-- | port/json.py | 33 | ||||
-rw-r--r-- | record.py | 1249 | ||||
-rw-r--r-- | recorded.py | 19 | ||||
-rw-r--r-- | recordtube.py | 68 | ||||
-rw-r--r-- | serialize.py | 312 | ||||
-rw-r--r-- | tray.py | 10 | ||||
-rw-r--r-- | ui.py | 2444 | ||||
-rw-r--r-- | utils.py | 66 |
31 files changed, 3680 insertions, 5317 deletions
@@ -13,8 +13,10 @@ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import gst - +import os import logging +import constants + logger = logging.getLogger('record:aplay.py') def play(file, done_cb=None): @@ -37,7 +39,7 @@ def play(file, done_cb=None): bus.connect('message::eos', eos_cb) bus.connect('message::error', error_cb) - player.props.uri = 'file://' + file + player.props.uri = 'file://' + os.path.join(constants.GFX_PATH, file) player.set_state(gst.STATE_PLAYING) @@ -1,108 +1,70 @@ -import gtk -import os import gobject -import rsvg -import gc +import gtk +from gettext import gettext as _ from sugar.graphics.palette import Palette from sugar.graphics.tray import TrayButton -from sugar.graphics.icon import Icon -from sugar.graphics import style -from constants import Constants +import constants import utils -class RecdButton(TrayButton, gobject.GObject): - def __init__(self, ui, recd): - TrayButton.__init__(self) - self.ui = ui - self.recd = recd - - img = self.getImg( ) - self.set_icon_widget( img ) - - self.ACTIVATE_COPY_ID = 0 - self.ACTIVATE_REMOVE_ID = 0 - self.setup_rollover_options( recd.title ) - - - def getImg( self ): - img = gtk.Image() - ipb = self.recd.getThumbPixbuf() - xoff = 8 - yoff = 8 - pb = None - if (self.recd.type == Constants.TYPE_PHOTO): - if (self.recd.buddy): - thumbPhotoSvg = utils.loadSvg(Constants.thumbPhotoSvgData, self.recd.colorStroke.hex, self.recd.colorFill.hex) - pb = thumbPhotoSvg.get_pixbuf() - else: - pb = Constants.thumbPhotoSvg.get_pixbuf() - - elif (self.recd.type == Constants.TYPE_VIDEO): - if (self.recd.buddy): - thumbVideoSvg = utils.loadSvg(Constants.thumbVideoSvgData, self.recd.colorStroke.hex, self.recd.colorFill.hex) - pb = thumbVideoSvg.get_pixbuf() - else: - pb = Constants.thumbVideoSvg.get_pixbuf() - - elif (self.recd.type == Constants.TYPE_AUDIO): - if (self.recd.buddy): - thumbAudioSvg = utils.loadSvg(Constants.thumbAudioSvgData, self.recd.colorStroke.hex, self.recd.colorFill.hex) - pb = thumbAudioSvg.get_pixbuf() - else: - pb = Constants.thumbAudioSvg.get_pixbuf() - - img.set_from_pixbuf(pb) - img.show() - ipb.composite(pb, xoff, yoff, ipb.get_width(), ipb.get_height(), xoff, yoff, 1, 1, gtk.gdk.INTERP_BILINEAR, 255) - img.set_from_pixbuf(pb) - - gc.collect() +class RecdButton(TrayButton): + __gsignals__ = { + 'remove-requested': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + 'copy-clipboard-requested': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + } - return img - - - def setButtClickedId( self, id ): - self.BUTT_CLICKED_ID = id + def __init__(self, recd): + super(RecdButton, self).__init__() + self._recd = recd + self.set_icon_widget(self.get_image()) + self._copy_menu_item_handler = None - def getButtClickedId( self ): - return self.BUTT_CLICKED_ID - - - def setup_rollover_options( self, info ): - palette = Palette(info) + palette = Palette(recd.title) self.set_palette(palette) - self.rem_menu_item = gtk.MenuItem( Constants.istrRemove ) - self.ACTIVATE_REMOVE_ID = self.rem_menu_item.connect('activate', self._itemRemoveCb) - palette.menu.append(self.rem_menu_item) - self.rem_menu_item.show() - - self.addCopyMenuItem() + 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 addCopyMenuItem( self ): - if (self.recd.buddy and not self.recd.downloadedFromBuddy): + def _add_copy_menu_item( self ): + if self._recd.buddy and not self._recd.downloadedFromBuddy: return - if (self.ACTIVATE_COPY_ID != 0): - return - - self.copy_menu_item = gtk.MenuItem( Constants.istrCopyToClipboard ) - self.ACTIVATE_COPY_ID = self.copy_menu_item.connect('activate', self._itemCopyToClipboardCb) - self.get_palette().menu.append(self.copy_menu_item) - self.copy_menu_item.show() - - def cleanUp( self ): - self.rem_menu_item.disconnect( self.ACTIVATE_REMOVE_ID ) - if (self.ACTIVATE_COPY_ID != 0): - self.copy_menu_item.disconnect( self.ACTIVATE_COPY_ID ) + 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) + 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 _itemRemoveCb(self, widget): - self.ui.deleteThumbSelection( self.recd ) + 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 _itemCopyToClipboardCb(self, widget): - self.ui.copyToClipboard( self.recd )
\ No newline at end of file + def _copy_clipboard_clicked(self, widget): + self.emit('copy-clipboard-requested') diff --git a/collab.py b/collab.py new file mode 100644 index 0000000..34a0c21 --- /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/color.py b/color.py deleted file mode 100644 index f544405..0000000 --- a/color.py +++ /dev/null @@ -1,71 +0,0 @@ -#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 gtk - -class Color: - - def __init__(self): - pass - - - def init_rgba(self, r, g, b, a): - self._ro = r - self._go = g - self._bo = b - self._ao = a; - self._r = self._ro / 255.0 - self._g = self._go / 255.0 - self._b = self._bo / 255.0 - self._a = self._ao / 255.0 - - self._opaque = False - if (self._a == 1): - self.opaque = True - - rgb_tup = ( self._ro, self._go, self._bo ) - self.hex = self.rgb_to_hex( rgb_tup ) - self.gColor = gtk.gdk.color_parse( self.hex ) - - - def init_gdk(self, col): - self.init_hex( col.get_html() ) - - - def init_hex(self, hex): - cTup = self.hex_to_rgb( hex ) - self.init_rgba( cTup[0], cTup[1], cTup[2], 255 ) - - - def get_int(self): - return int(self._a * 255) + (int(self._b * 255) << 8) + (int(self._g * 255) << 16) + (int(self._r * 255) << 24) - - - def rgb_to_hex(self, rgb_tup): - hexcolor = '#%02x%02x%02x' % rgb_tup - return hexcolor - - - def hex_to_rgb(self, h): - c = eval('0x' + h[1:]) - r = (c >> 16) & 0xFF - g = (c >> 8) & 0xFF - b = c & 0xFF - return (int(r), int(g), int(b))
\ No newline at end of file diff --git a/constants.py b/constants.py index 1c39e97..9b0ed4a 100644 --- a/constants.py +++ b/constants.py @@ -1,355 +1,46 @@ # -*- coding: UTF-8 -*- import os -import gtk -from gettext import gettext as gt -from gettext import ngettext -import hippo +from gettext import gettext as _ -import sugar.graphics.style from sugar.activity import activity -from instance import Instance -from sugar import profile -from color import Color -import utils -import cairo -import pango -import pangocairo +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") - -def istrMinutes(x): - return ngettext('%(1)s minute', '%(1)s minutes', int(x)) % {'1': int(x)} - - -def istrSeconds(x): - return ngettext('%(1)s second', '%(1)s seconds', int(x)) % {'1': int(x)} - - -class Constants: - - VERSION = 54 - - SERVICE = "org.laptop.Record" - IFACE = SERVICE - PATH = "/org/laptop/Record" - activityId = None - - recdTitle = "title" - recdTags = "tags" - recdTime = "time" - recdRecorderName = "photographer" - recdRecorderHash = "recorderHash" - recdColorStroke = "colorStroke" - recdColorFill = "colorFill" - recdHashKey = "hashKey" - recdBuddy = "buddy" - recdMediaMd5 = "mediaMd5" - recdThumbMd5 = "thumbMd5" - recdMediaBytes = "mediaBytes" - recdThumbBytes = "thumbBytes" - recdBase64Thumb = "base64Thumb" - recdDatastoreId = "datastoreId" - recdAudioImage = "audioImage" - recdAlbum = "album" - recdType = "type" - recdRecd = "recd" - recdRecordVersion = "version" - - keyName = "name" - keyMime = "mime" - keyExt = "ext" - keyIstr = "istr" - - MODE_PHOTO = 0 - MODE_VIDEO = 1 - MODE_AUDIO = 2 - TYPE_PHOTO = MODE_PHOTO - TYPE_VIDEO = MODE_VIDEO - TYPE_AUDIO = MODE_AUDIO - - TIMER_0 = 0 - TIMER_5 = 5 - TIMER_10 = 10 - TIMERS = [] - TIMERS.append(TIMER_0) - TIMERS.append(TIMER_5) - TIMERS.append(TIMER_10) - - DURATION_2 = 2 - DURATION_4 = 4 - DURATION_6 = 6 - DURATIONS = [] - DURATIONS.append(DURATION_2) - DURATIONS.append(DURATION_4) - DURATIONS.append(DURATION_6) - - colorBlack = Color() - colorBlack.init_rgba( 0, 0, 0, 255 ) - colorWhite = Color() - colorWhite.init_rgba( 255, 255, 255, 255 ) - colorRed = Color() - colorRed.init_rgba( 255, 0, 0, 255) - colorGreen = Color() - colorGreen.init_rgba( 0, 255, 0, 255) - colorBlue = Color() - colorBlue.init_rgba( 0, 0, 255, 255) - colorButton = Color() - colorButton.init_gdk( sugar.graphics.style.COLOR_BUTTON_GREY ) - - gfxPath = os.path.join(activity.get_bundle_path(), "gfx") - soundClick = os.path.join(gfxPath, 'photoShutter.wav') - - - #defensive method against variables not translated correctly - def _(s): - #todo: permanent variable - istrsTest = {} - for i in range (0,4): - istrsTest[str(i)] = str(i) - - i = s - try: - #test translating the string with many replacements - i = gt(s) - test = i % istrsTest - except: - #if it doesn't work, revert - i = s - - return i - - - istrActivityName = _('Record') - istrPhoto = _('Photo') - istrVideo = _('Video') - istrAudio = _('Audio') - istrTimelapse = _('Time Lapse') - istrAnimation = _('Animation') - istrPanorama = _('Panorama') - istrInterview= _('Interview') - #TRANS: photo by photographer, e.g., "Photo by Mary" - istrBy = _("%(1)s by %(2)s") - istrTitle = _('Title:') - istrRecorder = _('Recorder:') - istrDate = _('Date:') - istrTags = _('Tags:') - istrSaving = _('Saving') - istrFinishedRecording = _("Finished recording") - istrRemove = _("Remove") - istrStoppedRecording = _("Stopped recording") - istrCopyToClipboard = _("Copy to clipboard") - istrTimer = _("Timer:") - istrDuration = _("Duration:") - istrRemaining = _("Remaining:") - istrNow = _("Immediate") - istrPlay = _("Play") - istrPause = _("Pause") - istrAddFrame = _("Add frame") - istrRemoveFrame = _("Remove frame") - istrFramesPerSecond = _("%(1)s frames per second") - istrQuality = _("Quality") - istrDefault = _("Default") - istrHighQuality = _("High") - istrLowQuality = _("Low") - istrLargeFile = _("Large file") - istrSmallFile = _("Small file") - istrSilent = _("Silent") - istrRotate = _("Rotate") - istrWidth = _("Width") - istrHeight = _("Height") - istrClickToTakePicture = _("Click to take picture") - istrClickToAddPicture = _("Click to add picture") - #TRANS: Downloading Photo from Mary - istrDownloadingFrom = _("Downloading %(1)s from %(2)s") - #TRANS: Cannot download this Photo - istrCannotDownload = _("Cannot download this %(1)s") - #TRANS: Save Photo to: - istrSaveTo = _("Save %(1)s to:") - istrYourDiskIsFull = _("Your %(1)s is full") - istrJournal = _("Journal") - istrUSB = _("USB") - istrCompactFlash = _("SD Card") - istrPreferences = _("Preferences") - istrFreeSpace = _("Free space:") - #TRANS: 7 photos - istrBitrate = _("Bitrate") - istrMaxBitrate = _("Maximum Bitrate") - istrMinBitrate = _("Minumum Bitrate") - istrManageBitrate = _("Manage Bitrate") - istrBorder = _("Border") - istrCenter = _("Center") - istrFrames = _("Frames") - istrKeyframeAuto = _("Automatic keyframe detection") - istrKeyframeForce = _("Force keyframe") - istrKeyframeFrequency = _("Keyframe frequency") - istrKeyframeMinDist = _("Keyframe minimum distance") - istrKeyframeThreshold = _("Keyframe threshold") - istrNoiseSensitivity = _("Noise Sensitivity") - istrQuick = _("Quick") - istrSharpness = _("Sharpness") - istrCapacity = _("Capacity") - - mediaTypes = {} - mediaTypes[TYPE_PHOTO] = {keyName:"photo", keyMime:"image/jpeg", keyExt:"jpg", keyIstr:istrPhoto} - mediaTypes[TYPE_VIDEO] = {keyName:"video", keyMime:"video/ogg", keyExt:"ogg", keyIstr:istrVideo} - mediaTypes[TYPE_AUDIO] = {keyName:"audio", keyMime:"audio/ogg", keyExt:"ogg", keyIstr:istrAudio} - - thumbPhotoSvgData = None - thumbPhotoSvg = None - thumbVideoSvg = None - maxEnlargeSvg = None - maxReduceSvg = None - infoOnSvg = None - xoGuySvgData = None - - recImg = None - recRedImg = None - recCircleCairo = None - recInsensitiveImg = None - recPlayImg = None - recPauseImg = None - countdownImgs = {} - - dim_CONTROLBAR_HT = 55 - - keepFreeKbOnXo = 100000 - - def __init__( self, ca ): - self.__class__.activityId = ca._activity_id - - thumbPhotoSvgPath = os.path.join(self.__class__.gfxPath, 'object-photo.svg') - thumbPhotoSvgFile = open(thumbPhotoSvgPath, 'r') - self.__class__.thumbPhotoSvgData = thumbPhotoSvgFile.read() - self.__class__.thumbPhotoSvg = utils.loadSvg(self.__class__.thumbPhotoSvgData, Instance.colorStroke.hex, Instance.colorFill.hex) - thumbPhotoSvgFile.close() - - thumbVideoSvgPath = os.path.join(self.__class__.gfxPath, 'object-video.svg') - thumbVideoSvgFile = open(thumbVideoSvgPath, 'r') - self.__class__.thumbVideoSvgData = thumbVideoSvgFile.read() - self.__class__.thumbVideoSvg = utils.loadSvg(self.__class__.thumbVideoSvgData, Instance.colorStroke.hex, Instance.colorFill.hex) - thumbVideoSvgFile.close() - - thumbAudioSvgPath = os.path.join(self.__class__.gfxPath, 'object-audio.svg') - thumbAudioSvgFile = open(thumbAudioSvgPath, 'r') - self.__class__.thumbAudioSvgData = thumbAudioSvgFile.read() - self.__class__.thumbAudioSvg = utils.loadSvg(self.__class__.thumbAudioSvgData, Instance.colorStroke.hex, Instance.colorFill.hex) - thumbAudioSvgFile.close() - - maxEnlargeSvgPath = os.path.join(self.__class__.gfxPath, 'max-enlarge.svg') - maxEnlargeSvgFile = open(maxEnlargeSvgPath, 'r') - maxEnlargeSvgData = maxEnlargeSvgFile.read() - self.__class__.maxEnlargeSvg = utils.loadSvg(maxEnlargeSvgData, None, None ) - maxEnlargeSvgFile.close() - - maxReduceSvgPath = os.path.join(self.__class__.gfxPath, 'max-reduce.svg') - maxReduceSvgFile = open(maxReduceSvgPath, 'r') - maxReduceSvgData = maxReduceSvgFile.read() - self.__class__.maxReduceSvg = utils.loadSvg(maxReduceSvgData, None, None ) - maxReduceSvgFile.close() - - infoOnSvgPath = os.path.join(self.__class__.gfxPath, 'corner-info.svg') - infoOnSvgFile = open(infoOnSvgPath, 'r') - infoOnSvgData = infoOnSvgFile.read() - self.__class__.infoOnSvg = utils.loadSvg(infoOnSvgData, None, None ) - infoOnSvgFile.close() - - xoGuySvgPath = os.path.join(self.__class__.gfxPath, 'xo-guy.svg') - xoGuySvgFile = open(xoGuySvgPath, 'r') - self.__class__.xoGuySvgData = xoGuySvgFile.read() - xoGuySvgFile.close() - - recFile = os.path.join(self.__class__.gfxPath, 'media-record.png') - recPixbuf = gtk.gdk.pixbuf_new_from_file(recFile) - self.__class__.recImg = gtk.Image() - self.__class__.recImg.set_from_pixbuf( recPixbuf ) - - recRedFile = os.path.join(self.__class__.gfxPath, 'media-record-red.png') - recRedPixbuf = gtk.gdk.pixbuf_new_from_file(recRedFile) - self.__class__.recRedImg = gtk.Image() - self.__class__.recRedImg.set_from_pixbuf( recRedPixbuf ) - - recCircleFile = os.path.join(self.__class__.gfxPath, 'media-circle.png') - recCirclePixbuf = gtk.gdk.pixbuf_new_from_file(recCircleFile) - self.__class__.recCircleCairo = hippo.cairo_surface_from_gdk_pixbuf(recCirclePixbuf) - - recInsFile = os.path.join(self.__class__.gfxPath, 'media-insensitive.png') - recInsPixbuf = gtk.gdk.pixbuf_new_from_file(recInsFile) - self.__class__.recInsensitiveImg = gtk.Image() - self.__class__.recInsensitiveImg.set_from_pixbuf( recInsPixbuf ) - - fullInsFile = os.path.join(self.__class__.gfxPath, 'full-insensitive.png') - fullInsPixbuf = gtk.gdk.pixbuf_new_from_file(fullInsFile) - self.__class__.fullInsensitiveImg = gtk.Image() - self.__class__.fullInsensitiveImg.set_from_pixbuf( fullInsPixbuf ) - - recPlayFile = os.path.join(self.__class__.gfxPath, 'media-play.png') - recPlayPixbuf = gtk.gdk.pixbuf_new_from_file(recPlayFile) - self.__class__.recPlayImg = gtk.Image() - self.__class__.recPlayImg.set_from_pixbuf( recPlayPixbuf ) - - recPauseFile = os.path.join(self.__class__.gfxPath, 'media-pause.png') - recPausePixbuf = gtk.gdk.pixbuf_new_from_file(recPauseFile) - self.__class__.recPauseImg = gtk.Image() - self.__class__.recPauseImg.set_from_pixbuf( recPausePixbuf ) - - self._ts = self.__class__.TIMERS - longestTime = self._ts[len(self._ts)-1] - for i in range (0, longestTime): - self.createCountdownPng( i ) - - - def createCountdownPng(self, num): - todisk = True - - rendered = False - if (todisk): - path = os.path.join(Instance.dataPath, str(num)+".png") - if (os.path.exists(path)): - rendered = True - - - if (not rendered): - w = self.__class__.dim_CONTROLBAR_HT - h = w - if (todisk): - cimg = cairo.ImageSurface(cairo.FORMAT_ARGB32, w, h) - ctx = cairo.Context(cimg) - else: - pixmap = gtk.gdk.Pixmap(None, w, h, 24) - 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) - ctx.set_source_surface (self.__class__.recCircleCairo, 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) - - img = gtk.Image() - if (todisk): - path = os.path.join(Instance.dataPath, str(num)+".png") - if (not rendered): - path = utils.getUniqueFilepath(path, 0) - cimg.write_to_png(path) - numPixbuf = gtk.gdk.pixbuf_new_from_file(path) - img.set_from_pixbuf( numPixbuf ) - else: - img.set_from_pixmap(pixmap, None) - - self.__class__.countdownImgs[int(num)] = img diff --git a/gfx/full-insensitive.png b/gfx/full-insensitive.png Binary files differdeleted file mode 100644 index 82c833a..0000000 --- a/gfx/full-insensitive.png +++ /dev/null diff --git a/gfx/info-on.svg b/gfx/info-on.svg deleted file mode 100644 index 678784a..0000000 --- a/gfx/info-on.svg +++ /dev/null @@ -1,17 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?>
-<!-- Generator: Adobe Illustrator 13.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
- width="48.5px" height="48.5px" viewBox="0 0 48.5 48.5" enable-background="new 0 0 48.5 48.5" xml:space="preserve">
-
-<g>
- <rect x="1.75" y="1.75" fill="#808384" stroke="#FFFFFF" stroke-width="3.5" width="45" height="45"/>
- <g>
- <path fill="#FFFFFF" d="M23.7,32.6c-0.7,1.8-0.9,2.6-0.9,3.1c0,0.4,0.2,0.7,0.6,0.7c1-0.1,3.5-2.7,3.7-3.1c0.1-0.2,0.3-0.2,0.4,0
- l0.5,0.5c0.2,0.2,0.3,0.3,0.1,0.5c-0.3,0.4-3.6,5-8.9,5c-1,0-1.4-0.6-1.4-1.3c0-1,0.8-3.2,2.7-8.5l1.3-3.7
- c0.5-1.3,0.8-2.2,0.8-2.8c0-0.4-0.2-0.6-0.6-0.6c-0.8,0-3,2.5-3.6,3.3c-0.2,0.2-0.4,0.2-0.6,0l-0.5-0.4
- c-0.2-0.1-0.3-0.2-0.2-0.4c0.3-0.3,4.6-5.9,8.8-5.9c1.2,0,1.7,0.6,1.7,1.5s-0.6,2.7-1.4,5L23.7,32.6z M24.5,10.4
- c0-1.3,1.2-2.6,2.7-2.6c1.9,0,2.9,1.2,2.9,2.4c0,1.1-1.1,2.8-2.8,2.8C25.4,13,24.5,11.6,24.5,10.4z"/>
- </g>
-</g>
-</svg>
diff --git a/gfx/media-record.svg b/gfx/media-record.svg deleted file mode 100644 index 0adb08f..0000000 --- a/gfx/media-record.svg +++ /dev/null @@ -1,6 +0,0 @@ -<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [ - <!ENTITY stroke_color "#010101"> - <!ENTITY fill_color "#FFFFFF"> -]><svg enable-background="new 0 0 55 55" height="55px" version="1.1" viewBox="0 0 55 55" width="55px" x="0px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" y="0px"><g display="block" id="media-record"> - <path d="M27.497,5C15.073,5,4.999,15.075,4.999,27.5c0,12.427,10.074,22.5,22.498,22.5 c12.428,0,22.502-10.073,22.502-22.5C49.999,15.075,39.925,5,27.497,5z M27.501,35.389c-4.361,0-7.89-3.534-7.89-7.889 c0-4.356,3.528-7.889,7.89-7.889c4.357,0,7.889,3.532,7.889,7.889C35.39,31.854,31.858,35.389,27.501,35.389z" display="inline" fill="&fill_color;"/> -</g></svg>
\ No newline at end of file @@ -18,18 +18,14 @@ #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 pygtk -pygtk.require('2.0') -import sys import gst -import gst.interfaces import pygst pygst.require('0.10') -import time -import threading import gobject gobject.threads_init() @@ -37,12 +33,10 @@ from sugar.activity.activity import get_bundle_path import logging from instance import Instance -from constants import Constants -import record +import constants import utils -import ui -logger = logging.getLogger('record:glive.py') +logger = logging.getLogger('glive') OGG_TRAITS = { 0: { 'width': 160, 'height': 120, 'quality': 16 }, @@ -51,70 +45,70 @@ OGG_TRAITS = { THUMB_STUB = gtk.gdk.pixbuf_new_from_file( os.path.join(get_bundle_path(), 'gfx', 'stub.png')) -def _does_camera_present(): - v4l2src = gst.element_factory_make('v4l2src') - if v4l2src.props.device_name is None: - return False, False - - # 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) - can_limit_framerate = pipeline.set_state(gst.STATE_PAUSED) != gst.STATE_CHANGE_FAILURE - pipeline.set_state(gst.STATE_NULL) - return True, can_limit_framerate - -camera_presents, can_limit_framerate = _does_camera_present() - class Glive: - def __init__(self, pca): - self.window = None - self.ca = pca - self._eos_cb = None - - self.playing = False - self.picExposureOpen = False - - self.AUDIO_TRANSCODE_ID = 0 - self.TRANSCODE_ID = 0 - self.VIDEO_TRANSCODE_ID = 0 + PHOTO_MODE_PHOTO = 0 + PHOTO_MODE_AUDIO = 1 - self.PHOTO_MODE_PHOTO = 0 - self.PHOTO_MODE_AUDIO = 1 + def __init__(self, activity_obj, model): + self.activity = activity_obj + self.model = model + self._eos_cb = None - self.TRANSCODE_UPDATE_INTERVAL = 200 + 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.VIDEO_WIDTH_SMALL = 160 - self.VIDEO_HEIGHT_SMALL = 120 - self.VIDEO_FRAMERATE_SMALL = 10 + self._audio_pixbuf = None - self.VIDEO_WIDTH_LARGE = 200 - self.VIDEO_HEIGHT_LARGE = 150 - self.VIDEO_FRAMERATE_SMALL = 10 + self._detect_camera() - self.pipeline = gst.Pipeline("my-pipeline") - self.createPhotoBin() - self.createAudioBin() - self.createVideoBin() - self.createPipeline() + self._pipeline = gst.Pipeline("Record") + self._create_photobin() + self._create_audiobin() + self._create_videobin() + self._create_xbin() + self._create_pipeline() - self.thumbPipes = [] - self.muxPipes = [] + self._thumb_pipes = [] + self._mux_pipes = [] - bus = self.pipeline.get_bus() - bus.enable_sync_message_emission() + bus = self._pipeline.get_bus() bus.add_signal_watch() - self.SYNC_ID = bus.connect('sync-message::element', self._onSyncMessageCb) - self.MESSAGE_ID = bus.connect('message', self._onMessageCb) + 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 - def createPhotoBin ( self ): + 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) @@ -123,18 +117,18 @@ class Glive: jpeg = gst.element_factory_make("jpegenc", "pbjpeg") sink = gst.element_factory_make("fakesink", "pbsink") - self.HANDOFF_ID = sink.connect("handoff", self.copyPic) + sink.connect("handoff", self._photo_handoff) sink.set_property("signal-handoffs", True) - self.photobin = gst.Bin("photobin") - self.photobin.add(queue, colorspace, jpeg, sink) + 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)) + self._photobin.add_pad(gst.GhostPad("sink", pad)) - def createAudioBin ( self ): + 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 @@ -158,30 +152,30 @@ class Glive: 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) + 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) + 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 createVideoBin ( self ): + 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) + 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='+str(self.VIDEO_WIDTH_SMALL)+',height='+str(self.VIDEO_HEIGHT_SMALL)) + scalecaps = gst.Caps('video/x-raw-yuv,width=160,height=120') scalecapsfilter.set_property("caps", scalecaps) colorspace = gst.element_factory_make("ffmpegcolorspace", "vbcolorspace") @@ -194,8 +188,8 @@ class Glive: 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) + self._videobin = gst.Bin("videobin") + self._videobin.add(queue, scale, scalecapsfilter, colorspace, enc, mux, sink) queue.link(scale) scale.link_pads(None, scalecapsfilter, "sink") @@ -203,15 +197,31 @@ class Glive: gst.element_link_many(colorspace, enc, mux, sink) pad = queue.get_static_pad("sink") - self.videobin.add_pad(gst.GhostPad("sink", pad)) + 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 cfgVideoBin (self, quality, width, height): - vbenc = self.videobin.get_by_name("vbenc") + 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 = self._videobin.get_by_name("scalecaps") scaps.set_property("caps", gst.Caps("video/x-raw-yuv,width=%d,height=%d" % (width, height))) - def createPipeline ( self ): + def _create_pipeline(self): src = gst.element_factory_make("v4l2src", "camsrc") try: # old gst-plugins-good does not have this property @@ -222,8 +232,8 @@ class Glive: # 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 can_limit_framerate: - srccaps = gst.Caps('video/x-raw-yuv,framerate='+str(self.VIDEO_FRAMERATE_SMALL)+'/1') + if self._can_limit_framerate: + srccaps = gst.Caps('video/x-raw-yuv,framerate=10/1') else: srccaps = gst.Caps('video/x-raw-yuv') @@ -234,7 +244,7 @@ class Glive: # 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='+str(self.VIDEO_FRAMERATE_SMALL)+'/1') + ratecaps = gst.Caps('video/x-raw-yuv,framerate=10/1') tee = gst.element_factory_make("tee", "tee") queue = gst.element_factory_make("queue", "dispqueue") @@ -243,227 +253,200 @@ class Glive: queue.set_property("leaky", True) queue.set_property("max-size-buffers", 2) - self.pipeline.add(src, rate, tee, queue) + self._pipeline.add(src, rate, tee, queue) src.link(rate, srccaps) rate.link(tee, ratecaps) tee.link(queue) - xvsink = gst.element_factory_make("xvimagesink", "xvsink") - xv_available = xvsink.set_state(gst.STATE_PAUSED) != gst.STATE_CHANGE_FAILURE - xvsink.set_state(gst.STATE_NULL) + 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) - if xv_available: - # http://thread.gmane.org/gmane.comp.video.gstreamer.devel/29644 - xvsink.set_property("sync", False) + # http://thread.gmane.org/gmane.comp.video.gstreamer.devel/29644 + self._xvsink.set_property("sync", False) - self.pipeline.add(xvsink) - queue.link(xvsink) - else: - cspace = gst.element_factory_make("ffmpegcolorspace") - xsink = gst.element_factory_make("ximagesink") - - # http://thread.gmane.org/gmane.comp.video.gstreamer.devel/29644 - xsink.set_property("sync", False) - - self.pipeline.add(cspace, xsink) - gst.element_link_many(queue, cspace, xsink) + self._xvsink.set_property("force-aspect-ratio", True) - def log_queue_overrun(self, queue): + 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 thumbPipe(self): - return self.thumbPipes[ len(self.thumbPipes)-1 ] - - - def thumbEl(self, name): - return self.thumbPipe().get_by_name(name) - - - def muxPipe(self): - return self.muxPipes[ len(self.muxPipes)-1 ] - + def _thumb_element(self, name): + return self._thumb_pipes[-1].get_by_name(name) + + def _configure_xv(self): + if self._pipeline.get_by_name("xsink") == self._xvsink: + # 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 not self._has_camera: + return - def muxEl(self, name): - return self.muxPipe().get_by_name(name) + if self._get_state() == gst.STATE_PLAYING: + return + if use_xv and self._xv_available: + xsink = self._configure_xv() + else: + xsink = self._configure_x() - def play(self): - if not camera_presents: - return + # 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 + self._pipeline.set_state(gst.STATE_PLAYING) + self._playing = True def pause(self): - self.pipeline.set_state(gst.STATE_PAUSED) - self.playing = False - + self._pipeline.set_state(gst.STATE_PAUSED) + self._playing = False def stop(self): - self.pipeline.set_state(gst.STATE_NULL) - self.playing = False + self._pipeline.set_state(gst.STATE_NULL) + self._playing = False def is_playing(self): - return self.playing + return self._playing - def idlePlayElement(self, element): - element.set_state(gst.STATE_PLAYING) - return False + def _get_state(self): + return self._pipeline.get_state()[1] - def stopRecordingAudio( self ): + 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.pipeline.remove(self.audiobin) - self.pipeline.set_state(gst.STATE_PLAYING) - gobject.idle_add( self.stoppedRecordingAudio ) - + self._pipeline.set_state(gst.STATE_NULL) + self.model.shutter_sound() + self._pipeline.remove(self._audiobin) + self.play() - def stoppedRecordingVideo(self): - if ( len(self.thumbPipes) > 0 ): - thumbline = self.thumbPipes[len(self.thumbPipes)-1] - thumbline.get_by_name('thumbFakesink').disconnect(self.THUMB_HANDOFF_ID) - - oggFilepath = os.path.join(Instance.instancePath, "output.ogg") #ogv - if (not os.path.exists(oggFilepath)): - self.record = False - self.ca.m.cannotSaveVideo() - self.ca.m.stoppedRecordingVideo() + if not self._audio_pixbuf: + # FIXME: inform model of failure? return - oggSize = os.path.getsize(oggFilepath) - if (oggSize <= 0): - self.record = False - self.ca.m.cannotSaveVideo() - self.ca.m.stoppedRecordingVideo() + + 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 - line = 'filesrc location=' + str(oggFilepath) + ' name=thumbFilesrc ! oggdemux name=thumbOggdemux ! theoradec name=thumbTheoradec ! tee name=thumbTee ! queue name=thumbQueue ! ffmpegcolorspace name=thumbFfmpegcolorspace ! jpegenc name=thumbJPegenc ! fakesink name=thumbFakesink' - thumbline = gst.parse_launch(line) - thumbQueue = thumbline.get_by_name('thumbQueue') - thumbQueue.set_property("leaky", True) - thumbQueue.set_property("max-size-buffers", 1) - thumbTee = thumbline.get_by_name('thumbTee') - thumbFakesink = thumbline.get_by_name('thumbFakesink') - self.THUMB_HANDOFF_ID = thumbFakesink.connect("handoff", self.copyThumbPic) - thumbFakesink.set_property("signal-handoffs", True) - self.thumbPipes.append(thumbline) - self.thumbExposureOpen = True - gobject.idle_add( self.idlePlayElement, thumbline ) - - - def stoppedRecordingAudio( self ): - record.Record.log.debug("stoppedRecordingAudio") - if (self.audioPixbuf != None): - audioFilepath = os.path.join(Instance.instancePath, "output.wav")#self.el("audioFilesink").get_property("location") - if (not os.path.exists(audioFilepath)): - self.record = False - self.ca.m.cannotSaveVideo() - return - wavSize = os.path.getsize(audioFilepath) - if (wavSize <= 0): - self.record = False - self.ca.m.cannotSaveVideo() - return - - self.ca.ui.setPostProcessPixBuf(self.audioPixbuf) - - line = 'filesrc location=' + str(audioFilepath) + ' name=audioFilesrc ! wavparse name=audioWavparse ! audioconvert name=audioAudioconvert ! vorbisenc name=audioVorbisenc ! oggmux name=audioOggmux ! filesink name=audioFilesink' - audioline = gst.parse_launch(line) - - taglist = self.getTags(Constants.TYPE_AUDIO) - base64AudioSnapshot = utils.getStringFromPixbuf(self.audioPixbuf) - taglist[gst.TAG_EXTENDED_COMMENT] = "coverart="+str(base64AudioSnapshot) - vorbisEnc = audioline.get_by_name('audioVorbisenc') - vorbisEnc.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_ID = audioBus.connect('message', self._onMuxedAudioMessageCb, audioline) - self.TRANSCODE_ID = gobject.timeout_add(self.TRANSCODE_UPDATE_INTERVAL, self._transcodeUpdateCb, audioline) - gobject.idle_add( self.idlePlayElement, audioline ) - else: - self.record = False - self.ca.m.cannotSaveVideo() + 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) - def getTags( self, type ): + taglist = self._get_tags(constants.TYPE_AUDIO) + 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] = str(Instance.nickName) + 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.mediaTypes[type][Constants.keyIstr] - tl[gst.TAG_TITLE] = Constants.istrBy % {"1":stringType, "2":str(Instance.nickName)} + 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 _takePhoto(self): - if self.picExposureOpen: + def _take_photo(self, photo_mode): + if self._pic_exposure_open: return - self.picExposureOpen = True - pad = self.photobin.get_static_pad("sink") + 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) + 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 takePhoto(self): - if not camera_presents: - return + def take_photo(self): + if self._has_camera: + self._take_photo(self.PHOTO_MODE_PHOTO) - self.photoMode = self.PHOTO_MODE_PHOTO - self._takePhoto() - - def copyPic(self, fsink, buffer, pad, user_data=None): - if not self.picExposureOpen: + 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 = 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) + self._pipeline.get_by_name("tee").unlink(self._photobin) + self._pipeline.remove(self._photobin) pad.set_blocked_async(False, self.blockedCb, None) - self.picExposureOpen = False + 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.savePhoto( pixBuf ) - + self.save_photo(pixBuf) - def savePhoto(self, pixbuf): - if self.photoMode == self.PHOTO_MODE_AUDIO: - self.audioPixbuf = pixbuf + def save_photo(self, pixbuf): + if self._photo_mode == self.PHOTO_MODE_AUDIO: + self._audio_pixbuf = pixbuf else: - self.ca.m.savePhoto(pixbuf) - + self.model.save_photo(pixbuf) - def startRecordingVideo(self, quality): - if not camera_presents: + def record_video(self, quality): + if not self._has_camera: return - self.record = True - self.ogg_quality = quality - self.cfgVideoBin (OGG_TRAITS[quality]['quality'], + self._ogg_quality = quality + self._config_videobin(OGG_TRAITS[quality]['quality'], OGG_TRAITS[quality]['width'], OGG_TRAITS[quality]['height']) @@ -472,90 +455,106 @@ class Glive: # 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.pipeline.set_state(gst.STATE_PLAYING) - - def startRecordingAudio(self): - self.audioPixbuf = None - - self.photoMode = self.PHOTO_MODE_AUDIO - self._takePhoto() + 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() - self.record = True + def record_audio(self): + 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.pipeline.set_state(gst.STATE_PLAYING) + self._pipeline.set_state(gst.STATE_NULL) + self._pipeline.add(self._audiobin) + self.play() - def stopRecordingVideo(self): - if not camera_presents: + 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 - self._eos_cb = self.stopRecordingVideoEOS - 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 stopRecordingVideoEOS(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.pipeline.set_state(gst.STATE_PLAYING) - gobject.idle_add( self.stoppedRecordingVideo ) + # 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.thumbExposureOpen: + if not self._thumb_exposure_open: return - self.thumbExposureOpen = False - pic = gtk.gdk.pixbuf_loader_new_with_mime_type("image/jpeg") - pic.write(buffer) - pic.close() - self.thumbBuf = pic.get_pixbuf() - del pic - self.thumbEl('thumbTee').unlink(self.thumbEl('thumbQueue')) + 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) - oggFilepath = os.path.join(Instance.instancePath, "output.ogg") #ogv - self.ca.ui.setPostProcessPixBuf(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.getTags(Constants.TYPE_VIDEO) - vorbisEnc = muxline.get_by_name('muxVorbisenc') - vorbisEnc.merge_tags(taglist, gst.TAG_MERGE_REPLACE_ALL) + 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_ID = muxBus.connect('message', self._onMuxedVideoMessageCb, muxline) - self.muxPipes.append(muxline) + 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(self.TRANSCODE_UPDATE_INTERVAL, self._transcodeUpdateCb, muxline) + self._transcode_id = gobject.timeout_add(200, self._transcodeUpdateCb, muxline) muxline.set_state(gst.STATE_PLAYING) def _transcodeUpdateCb( self, pipe ): - position, duration = self.queryPosition( pipe ) + position, duration = self._query_position( pipe ) if position != gst.CLOCK_TIME_NONE: value = position * 100.0 / duration value = value/100.0 - self.ca.ui.progressWindow.updateProgress(value, Constants.istrSaving) + self.model.set_progress(value, _('Saving...')) return True - - def queryPosition( self, pipe ): + def _query_position(self, pipe): try: position, format = pipe.query_position(gst.FORMAT_TIME) except: @@ -568,64 +567,45 @@ class Glive: return (position, duration) - def _onMuxedVideoMessageCb(self, bus, message, pipe): - t = message.type - if (t == gst.MESSAGE_EOS): - self.record = False - gobject.source_remove(self.VIDEO_TRANSCODE_ID) - self.VIDEO_TRANSCODE_ID = 0 - gobject.source_remove(self.TRANSCODE_ID) - self.TRANSCODE_ID = 0 - 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 ) - ogg_w = OGG_TRAITS[self.ogg_quality]['width'] - ogg_h = OGG_TRAITS[self.ogg_quality]['height'] - self.ca.m.saveVideo(self.thumbBuf, str(muxFilepath), ogg_w, ogg_h) - self.ca.m.stoppedRecordingVideo() - return False - else: + 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): - t = message.type - if (t == gst.MESSAGE_EOS): - record.Record.log.debug("audio gst.MESSAGE_EOS") - self.record = False - gobject.source_remove(self.AUDIO_TRANSCODE_ID) - self.AUDIO_TRANSCODE_ID = 0 - gobject.source_remove(self.TRANSCODE_ID) - self.TRANSCODE_ID = 0 - 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.ca.m.saveAudio(oggFilepath, self.audioPixbuf) - return False - else: + 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() - def _onSyncMessageCb(self, bus, message): - if message.structure is None: - return - if message.structure.get_name() == 'prepare-xwindow-id': - self.window.set_sink(message.src) - message.src.set_property('force-aspect-ratio', True) - + 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 _onMessageCb(self, bus, message): + def _bus_message_handler(self, bus, message): t = message.type if t == gst.MESSAGE_EOS: if self._eos_cb: @@ -641,48 +621,23 @@ class Glive: def abandonMedia(self): self.stop() - if (self.AUDIO_TRANSCODE_ID != 0): - gobject.source_remove(self.AUDIO_TRANSCODE_ID) - self.AUDIO_TRANSCODE_ID = 0 - if (self.TRANSCODE_ID != 0): - gobject.source_remove(self.TRANSCODE_ID) - self.TRANSCODE_ID = 0 - if (self.VIDEO_TRANSCODE_ID != 0): - gobject.source_remove(self.VIDEO_TRANSCODE_ID) - self.VIDEO_TRANSCODE_ID = 0 - - wavFilepath = os.path.join(Instance.instancePath, "output.wav") - if (os.path.exists(wavFilepath)): - os.remove(wavFilepath) - oggFilepath = os.path.join(Instance.instancePath, "output.ogg") #ogv - if (os.path.exists(oggFilepath)): - os.remove(oggFilepath) - muxFilepath = os.path.join(Instance.instancePath, "mux.ogg") #ogv - if (os.path.exists(muxFilepath)): - os.remove(muxFilepath) - - -class LiveVideoWindow(gtk.Window): - def __init__(self, bgd ): - gtk.Window.__init__(self) - - self.imagesink = None - self.glive = None - - self.modify_bg( gtk.STATE_NORMAL, bgd ) - self.modify_bg( gtk.STATE_INSENSITIVE, bgd ) - self.unset_flags(gtk.DOUBLE_BUFFERED) - self.set_flags(gtk.APP_PAINTABLE) - - def set_glive(self, pglive): - self.glive = pglive - self.glive.window = self - - def set_sink(self, sink): - if (self.imagesink != None): - assert self.window.xid - self.imagesink = None - del self.imagesink + 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) - self.imagesink = sink - self.imagesink.set_xwindow_id(self.window.xid) @@ -18,114 +18,99 @@ #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN #THE SOFTWARE. -#look at jukeboxactivity.py - -import gtk -import pygtk -pygtk.require('2.0') -import sys +import gobject +gobject.threads_init() import pygst pygst.require('0.10') import gst -import gst.interfaces -import gobject -import time -gobject.threads_init() import logging logger = logging.getLogger('record:gplay.py') -import record - -class Gplay: +class Gplay(gobject.GObject): + __gsignals__ = { + 'playback-status-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_INT, gobject.TYPE_FLOAT)), + } - def __init__(self, ca): - self.ca = ca - self.window = None - self.players = [] - self.playing = False + 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') - self.player = gst.element_factory_make('playbin') - - bus = self.player.get_bus() - bus.enable_sync_message_emission() + bus = self._player.get_bus() bus.add_signal_watch() - bus.connect('message', self._onMessageCb) - - def _onMessageCb(self, bus, message): - if message.type == gst.MESSAGE_ERROR: - err, debug = message.parse_error() - logger.error('_onMessageCb: error=%s debug=%s' % (err, debug)) - - def setLocation(self, location): - if (self.player.get_property('uri') == location): - self.seek(gst.SECOND*0) - return - - self.player.set_state(gst.STATE_READY) - self.player.set_property('uri', location) - ext = location[len(location)-3:] - record.Record.log.debug("setLocation: ext->"+str(ext)) - if (ext == "jpg"): - self.pause() + 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 queryPosition(self): - "Returns a (position, duration) tuple" - try: - position, format = self.player.query_position(gst.FORMAT_TIME) - except: - position = gst.CLOCK_TIME_NONE + def _bus_eos(self, bus, message): + self.stop() - try: - duration, format = self.player.query_duration(gst.FORMAT_TIME) - except: - duration = gst.CLOCK_TIME_NONE + def set_location(self, location): + if self._player.get_property('uri') == location: + self.seek(0) + return - return (position, duration) + 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) - def seek(self, location): 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) + res = self._player.send_event(event) if res: - self.player.set_new_stream_time(0L) - + self._player.set_new_stream_time(0L) def pause(self): - self.playing = False - self.player.set_state(gst.STATE_PAUSED) - + self._player.set_state(gst.STATE_PAUSED) def play(self): - if not self.player.props.video_sink: + 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.player.props.video_sink.set_xwindow_id(self.window.window.xid) - self.playing = True - self.player.set_state(gst.STATE_PLAYING) + 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) - def stop(self): - self.playing = False - self.player.set_state(gst.STATE_NULL) + 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 - def get_state(self, timeout=1): - return self.player.get_state(timeout=timeout) + 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 is_playing(self): - return self.playing + 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 -class PlayVideoWindow(gtk.Window): - def __init__(self, bgd): - gtk.Window.__init__(self) + self._player.set_state(gst.STATE_NULL) + self._emit_playback_status(0) - self.modify_bg( gtk.STATE_NORMAL, bgd ) - self.modify_bg( gtk.STATE_INSENSITIVE, bgd ) - self.unset_flags(gtk.DOUBLE_BUFFERED) - self.set_flags(gtk.APP_PAINTABLE) diff --git a/greplay.py b/greplay.py deleted file mode 100644 index 67758a1..0000000 --- a/greplay.py +++ /dev/null @@ -1,80 +0,0 @@ -#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 gst -import gst.interfaces -import pygst -pygst.require('0.10') -import gobject -import os - -import record -import utils - -class Greplay(gobject.GObject): - - __gsignals__ = { - 'coverart-found': - (gobject.SIGNAL_RUN_FIRST, None, [object]) - } - - - def findAlbumArt( self, path ): - record.Record.log.debug("getAlbumArt") - if (path == None): - record.Record.log.debug("getAlbumArt: path==None") - self.emit('coverart-found', None) - return - if (not os.path.exists(path)): - record.Record.log.debug("getAlbumArt: path doesn't exist") - self.emit('coverart-found', None) - return - - self.pp = gst.parse_launch("filesrc location="+str(path)+" ! oggdemux ! vorbisdec ! fakesink") - self.pp.get_bus().add_signal_watch() - self.pp.get_bus().connect("message", self._onMessageCb) - self.pp.set_state(gst.STATE_PLAYING) - - - def _onMessageCb(self, bus, message): - t = message.type - if t == gst.MESSAGE_EOS: - record.Record.log.debug("Greplay:MESSAGE_EOS") - self.emit('coverart-found', None) - self.pp.set_state(gst.STATE_NULL) - return False - elif t == gst.MESSAGE_ERROR: - record.Record.log.debug("Greplay:MESSAGE_ERROR") - self.emit('coverart-found', None) - self.pp.set_state(gst.STATE_NULL) - return False - elif t == gst.MESSAGE_TAG: - tags = message.parse_tag() - for tag in tags.keys(): - if (str(tag) == "extended-comment"): - record.Record.log.debug("Found the tag!") - #todo, check for tagname - base64imgString = str(tags[tag])[len("coverart="):] - - pixbuf = utils.getPixbufFromString(base64imgString) - self.pp.set_state(gst.STATE_NULL) - self.emit('coverart-found', pixbuf) - return False - return True diff --git a/instance.py b/instance.py index 085c2da..bcee466 100644 --- a/instance.py +++ b/instance.py @@ -2,44 +2,21 @@ import os from sugar import profile from sugar import util -from sugar.activity import activity -import shutil - -from color import Color -import record class Instance: key = profile.get_pubkey() keyHash = util.sha_data(key) keyHashPrintable = util.printable_hash(keyHash) - nickName = profile.get_nick_name() - - colorFill = Color() - colorFill.init_hex( profile.get_color().get_fill_color() ) - colorStroke = Color() - colorStroke.init_hex( profile.get_color().get_stroke_color() ) - instanceId = None instancePath = None - dataPath = None def __init__(self, ca): - self.__class__.instanceId = ca._activity_id - self.__class__.instancePath = os.path.join(ca.get_activity_root(), "instance") recreateTmp() - self.__class__.dataPath = os.path.join(ca.get_activity_root(), "data") - recreateData() - def recreateTmp(): if (not os.path.exists(Instance.instancePath)): os.makedirs(Instance.instancePath) - -def recreateData(): - if (not os.path.exists(Instance.dataPath)): - os.makedirs(Instance.dataPath) - diff --git a/mediaview.py b/mediaview.py new file mode 100644 index 0000000..670a0fa --- /dev/null +++ b/mediaview.py @@ -0,0 +1,512 @@ +import os +from gettext import gettext as _ + +import gobject +import gtk +from gtk import gdk + +import constants +import utils + +COLOR_BLACK = gdk.color_parse('#000000') +COLOR_WHITE = gdk.color_parse('#ffffff') +COLOR_GREY = gdk.color_parse('#808080') + +class XoIcon(gtk.Image): + def __init__(self): + super(XoIcon, self).__init__() + + def set_colors(self, stroke, fill): + pixbuf = utils.load_colored_svg('xo-guy.svg', stroke, fill) + self.set_from_pixbuf(pixbuf) + + +class InfoView(gtk.EventBox): + """ + A metadata view/edit widget, that presents a primary view area in the top + right and a secondary view area in the bottom left. + """ + __gsignals__ = { + 'primary-allocated': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)), + 'secondary-allocated': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)), + 'tags-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_OBJECT,)), + } + + def __init__(self): + super(InfoView, self).__init__() + self.modify_bg(gtk.STATE_NORMAL, COLOR_GREY) + + self.connect('size-allocate', self._size_allocate) + + self._outer_vbox = gtk.VBox(spacing=7) + self.add(self._outer_vbox) + + hbox = gtk.HBox() + self._outer_vbox.pack_start(hbox, expand=True, fill=True) + + inner_vbox = gtk.VBox(spacing=5) + hbox.pack_start(inner_vbox, expand=True, fill=True, padding=6) + + author_hbox = gtk.HBox() + inner_vbox.pack_start(author_hbox, expand=False) + + label = gtk.Label() + label.set_markup('<b>' + _('Author:') + '</b>') + author_hbox.pack_start(label, expand=False) + + self._xo_icon = XoIcon() + author_hbox.pack_start(self._xo_icon, expand=False) + + self._author_label = gtk.Label() + author_hbox.pack_start(self._author_label, expand=False) + + self._date_label = gtk.Label() + self._date_label.set_line_wrap(True) + alignment = gtk.Alignment(0.0, 0.5, 0.0, 0.0) + alignment.add(self._date_label) + inner_vbox.pack_start(alignment, expand=False) + + label = gtk.Label() + label.set_markup('<b>' + _('Tags:') + '</b>') + alignment = gtk.Alignment(0.0, 0.5, 0.0, 0.0) + alignment.add(label) + inner_vbox.pack_start(alignment, expand=False) + + textview = gtk.TextView() + self._tags_buffer = textview.get_buffer() + self._tags_buffer.connect('changed', self._tags_changed) + inner_vbox.pack_start(textview, expand=True, fill=True) + + # the main viewing widget will be painted exactly on top of this one + alignment = gtk.Alignment(1.0, 0.0, 0.0, 0.0) + self._view_bg = gtk.EventBox() + self._view_bg.modify_bg(gtk.STATE_NORMAL, COLOR_BLACK) + alignment.add(self._view_bg) + hbox.pack_start(alignment, expand=False) + + # the secondary viewing widget will be painted exactly on top of this one + alignment = gtk.Alignment(0.0, 1.0, 0.0, 0.0) + self._live_bg = gtk.EventBox() + self._live_bg.modify_bg(gtk.STATE_NORMAL, COLOR_BLACK) + alignment.add(self._live_bg) + self._outer_vbox.pack_start(alignment, expand=False) + + def fit_to_allocation(self, allocation): + # main viewing area: 50% of each dimension + scale = 0.5 + w = int(allocation.width * scale) + h = int(allocation.height * scale) + self._view_bg.set_size_request(w, h) + + # live area: 1/4 of each dimension + scale = 0.25 + w = int(allocation.width * scale) + h = int(allocation.height * scale) + self._live_bg.set_size_request(w, h) + + def show(self): + self.show_all() + + def hide(self): + self.hide_all() + + def set_author(self, name, stroke, fill): + self._xo_icon.set_colors(stroke, fill) + self._author_label.set_text(name) + + def set_date(self, date): + self._date_label.set_markup('<b>' + _('Date:') + '</b> ' + date) + + def set_tags(self, tags): + self._tags_buffer.set_text(tags) + + def _size_allocate(self, widget, allocation): + self.emit('primary-allocated', self._view_bg.allocation) + self.emit('secondary-allocated', self._live_bg.allocation) + + def _tags_changed(self, widget): + self.emit('tags-changed', widget) + +class VideoBox(gtk.EventBox): + """ + A widget with its own window for rendering a gstreamer video sink onto. + """ + def __init__(self): + super(VideoBox, self).__init__() + self.unset_flags(gtk.DOUBLE_BUFFERED) + self.set_flags(gtk.APP_PAINTABLE) + self._sink = None + self._xid = None + self.connect('realize', self._realize) + + def _realize(self, widget): + self._xid = self.window.xid + + def do_expose_event(self): + if self._sink: + self._sink.expose() + return False + else: + return True + + # can be called from gstreamer thread, must not do any GTK+ stuff + def set_sink(self, sink): + self._sink = sink + sink.set_xwindow_id(self._xid) + +class FullscreenButton(gtk.EventBox): + def __init__(self): + super(FullscreenButton, self).__init__() + + path = os.path.join(constants.GFX_PATH, 'max-reduce.svg') + self._enlarge_pixbuf = gdk.pixbuf_new_from_file(path) + self.width = self._enlarge_pixbuf.get_width() + self.height = self._enlarge_pixbuf.get_height() + + path = os.path.join(constants.GFX_PATH, 'max-enlarge.svg') + self._reduce_pixbuf = gdk.pixbuf_new_from_file_at_size(path, self.width, self.height) + + self._image = gtk.Image() + self.set_enlarge() + self._image.show() + self.add(self._image) + + def set_enlarge(self): + self._image.set_from_pixbuf(self._enlarge_pixbuf) + + def set_reduce(self): + self._image.set_from_pixbuf(self._reduce_pixbuf) + + +class InfoButton(gtk.EventBox): + def __init__(self): + super(InfoButton, self).__init__() + + path = os.path.join(constants.GFX_PATH, 'corner-info.svg') + pixbuf = gdk.pixbuf_new_from_file(path) + self.width = pixbuf.get_width() + self.height = pixbuf.get_height() + + self._image = gtk.image_new_from_pixbuf(pixbuf) + self._image.show() + self.add(self._image) + + +class ImageBox(gtk.EventBox): + def __init__(self): + super(ImageBox, self).__init__() + self._pixbuf = None + self._image = gtk.Image() + self.add(self._image) + + def show(self): + self._image.show() + super(ImageBox, self).show() + + def hide(self): + self._image.hide() + super(ImageBox, self).hide() + + def clear(self): + self._image.clear() + self._pixbuf = None + + def set_pixbuf(self, pixbuf): + self._pixbuf = pixbuf + + def set_size(self, width, height): + if 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): + 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() + @@ -19,345 +19,316 @@ #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 urllib -import string -import fnmatch import os -import random -import cairo -import gtk -import gtk.gdk -import pygtk -pygtk.require('2.0') -import gc -import math import time -import gobject -import operator +import json -import logging -logger = logging.getLogger('record:model.py') +import gobject +import gst +import sugar.profile import sugar.env -from constants import Constants +import aplay +import constants from instance import Instance from recorded import Recorded -from color import Color -from ui import UI import utils -import record import serialize +from collab import RecordCollab +from glive import Glive +from gplay import Gplay +logger = logging.getLogger('model') class Model: - def __init__( self, pca ): - self.ca = pca - self.MODE = Constants.MODE_PHOTO - self.UPDATING = True - self.RECORDING = False - self.FULL = self._isXoFull() + 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.mediaTypes.items(): + 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 - def updateXoFullStatus( self ): - self.FULL = self._isXoFull() - - - def _isXoFull( self ): - full = False - if (utils.getFreespaceKb() <= Constants.keepFreeKbOnXo): - full = True - - return full - - - def getRecdByMd5( self, md5 ): - for mh in range (0, len(self.mediaHashs)): - for r in range (0, len(self.mediaHashs[mh])): - recd = self.mediaHashs[mh][r] - if (recd.thumbMd5 == md5): - return recd - elif (recd.mediaMd5 == md5): - return recd - - return None - + 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)) - def isVideoMode( self ): - return self.MODE == Constants.MODE_VIDEO + 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 isPhotoMode( self ): - return self.MODE == Constants.MODE_PHOTO + def get_nickname(self): + return sugar.profile.get_nick_name() + def get_mode(self): + return self._mode - def displayThumb( self, recd, forceUpdating ): - #to avoid Xlib: unexpected async reply error when taking a picture on a gst callback, always call with idle_add - #this happens b/c this might get called from a gstreamer callback - if (not recd.type == self.MODE): + def change_mode(self, mode): + if mode == self._mode: return - if (forceUpdating): - self.setUpdating( True ) - hash = self.mediaHashs[recd.type] - if (len(hash) > 0): - self.ca.ui.addThumb(recd, forceUpdating) - if (forceUpdating): - self.setUpdating( False ) - - - def setupMode( self, type, update ): - if (not type == self.MODE): - return - - self.setUpdating( True ) - self.ca.ui.removeThumbs() - hash = self.mediaHashs[type] - for i in range (0, len(hash)): - self.ca.ui.addThumb( hash[i], True ) - if (update): - self.ca.ui.updateModeChange() - self.setUpdating(False) - - - def showNextThumb( self, shownRecd ): - if (shownRecd == None): - self.showLastThumb() - else: - hash = self.mediaHashs[self.MODE] - if (len(hash) > 0): - hash = self.mediaHashs[self.MODE] - i = operator.indexOf( hash, shownRecd ) - i = i+1 - if (i>=len(hash)): - i = 0 - self.ca.ui.showThumbSelection( hash[i] ) - - - def showPrevThumb( self, shownRecd ): - if (shownRecd == None): - self.showLastThumb() - else: - hash = self.mediaHashs[self.MODE] - if (len(hash) > 0): - hash = self.mediaHashs[self.MODE] - i = operator.indexOf( hash, shownRecd ) - i = i-1 - if (i<0): - i = len(hash)-1 - self.ca.ui.showThumbSelection( hash[i] ) + self._mode = mode + self.activity.remove_all_thumbnails() + for recd in self.mediaHashs[mode]: + self.activity.add_thumbnail(recd, True) - def showLastThumb( self ): - hash = self.mediaHashs[self.MODE] - if (len(hash) > 0): - self.ca.ui.showThumbSelection( hash[len(hash)-1] ) + self.set_state(constants.STATE_READY) + if mode == constants.MODE_PHOTO: + self.glive.play() - def doShutter( self ): - if (self.UPDATING): - return + def ui_frozen(self): + return not self._state == constants.STATE_READY - if (self.MODE == Constants.MODE_PHOTO): - self.startTakingPhoto() - elif (self.MODE == Constants.MODE_VIDEO): - if (not self.RECORDING): - self.startRecordingVideo() - else: - #post-processing begins now, so queue up this gfx - self.ca.ui.showPostProcessGfx(True) - self.stopRecordingVideo() - elif (self.MODE == Constants.MODE_AUDIO): - if (not self.RECORDING): - self.startRecordingAudio() - else: - #post-processing begins now, so queue up this gfx - self.ca.ui.showPostProcessGfx(True) - self.stopRecordingAudio() - - - def stopRecordingAudio( self ): - gobject.source_remove( self.ca.ui.UPDATE_DURATION_ID ) - self.ca.ui.progressWindow.updateProgress( 0, "" ) - self.setUpdating( True ) - self.setRecording( False ) - self.ca.ui.FULLSCREEN = False - self.ca.ui.updateVideoComponents() - - self.ca.glive.stopRecordingAudio( ) - - - def saveAudio( self, tmpPath, pixbuf ): - self.setUpdating( True ) - - recd = self.createNewRecorded( Constants.TYPE_AUDIO ) - os.rename( tmpPath, os.path.join(Instance.instancePath,recd.mediaFilename)) - - thumbPath = os.path.join(Instance.instancePath, recd.thumbFilename) - scale = float((UI.dim_THUMB_WIDTH+0.0)/(pixbuf.get_width()+0.0)) - thumbImg = utils.generateThumbnail(pixbuf, scale, UI.dim_THUMB_WIDTH, UI.dim_THUMB_HEIGHT) - thumbImg.write_to_png(thumbPath) - - imagePath = os.path.join(Instance.instancePath, "audioPicture.png") - imagePath = utils.getUniqueFilepath( imagePath, 0 ) - pixbuf.save( imagePath, "png", {} ) - recd.audioImageFilename = os.path.basename(imagePath) - - #at this point, we have both audio and thumb sapath, so we can save the recd - self.createNewRecordedMd5Sums( recd ) + def set_state(self, state): + self._state = state - audioHash = self.mediaHashs[Constants.TYPE_AUDIO] - audioHash.append( recd ) - gobject.idle_add(self.displayThumb, recd, True) - self.doPostSaveVideo() - self.meshShareRecd( recd ) + if state == constants.STATE_READY: + self.gplay.stop() + self.glive.play() + self.activity.set_state(state) - def startRecordingVideo( self ): - self.setUpdating( True ) - self.setRecording( True ) - #let the red eye kick in before we start the video underway - gobject.idle_add( self.beginRecordingVideo ) + def get_state(self): + return self._state + def set_progress(self, value, text): + self.activity.set_progress(value, text) - def beginRecordingVideo( self ): - self.ca.ui.recordVideo() - self.setUpdating( False ) + 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) - def startRecordingAudio( self ): - self.setUpdating( True ) - self.setRecording( True ) - self.ca.ui.recordAudio() - self.setUpdating( False ) + 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 - def setUpdating( self, upd ): - self.UPDATING = upd - self.ca.ui.updateButtonSensitivities() + return True + def _start_media_capture(self): + if self._mode == constants.MODE_PHOTO: + self.activity.set_shutter_sensitive(False) + self.glive.take_photo() + return - def setRecording( self, rec ): - self.RECORDING = rec - self.ca.ui.updateButtonSensitivities() + 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) - def stopRecordingVideo( self ): - self.ca.glive.stopRecordingVideo() - gobject.source_remove( self.ca.ui.UPDATE_DURATION_ID ) - self.setUpdating( True ) - self.setRecording( False ) - self.ca.ui.FULLSCREEN = False - self.ca.ui.updateVideoComponents() + 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 - def saveVideo( self, pixbuf, tmpPath, wid, hit ): - recd = self.createNewRecorded( Constants.TYPE_VIDEO ) - os.rename( tmpPath, os.path.join(Instance.instancePath,recd.mediaFilename)) + self.set_progress(0, '') - thumbPath = os.path.join(Instance.instancePath, recd.thumbFilename) - scale = float((UI.dim_THUMB_WIDTH+0.0)/(wid+0.0)) - thumbImg = utils.generateThumbnail(pixbuf, scale, UI.dim_THUMB_WIDTH, UI.dim_THUMB_HEIGHT) - thumbImg.write_to_png(thumbPath) + if self._mode == constants.MODE_VIDEO: + self.glive.stop_recording_video() + elif self._mode == constants.MODE_AUDIO: + self.glive.stop_recording_audio() - self.createNewRecordedMd5Sums( recd ) + self.set_state(constants.STATE_PROCESSING) - videoHash = self.mediaHashs[Constants.TYPE_VIDEO] - videoHash.append( recd ) - self.doPostSaveVideo() - gobject.idle_add(self.displayThumb, recd, True) - self.meshShareRecd( recd ) + def shutter_sound(self, done_cb=None): + aplay.play('photoShutter.wav', done_cb) + def _countdown_tick(self): + self._countdown_value = self._countdown_value - 1 + value = self._countdown_value + self.activity.set_countdown(value) - def meshShareRecd( self, recd ): - #hey, i just took a cool video.audio.photo! let me show you! - if (self.ca.recTube != None): - logger.debug('meshShareRecd') - recdXml = serialize.getRecdXmlMeshString(recd) - self.ca.recTube.notifyBudsOfNewRecd( Instance.keyHashPrintable, recdXml ) + if value <= 0: + self._countdown_handle = None + self._countdown_value = 0 + self.shutter_sound(self._start_media_capture) + return False + return True - def cannotSaveVideo( self ): - self.doPostSaveVideo() + 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 - def doPostSaveVideo( self ): - self.ca.ui.showPostProcessGfx(False) + # otherwise, capture normally + self.shutter_sound(self._start_media_capture) - #prep the ui for your return - self.ca.ui.LAST_MODE = -1 - self.ca.ui.TRANSCODING = False + # called from gstreamer thread + def still_ready(self, pixbuf): + gobject.idle_add(self.activity.show_still, pixbuf) - #resume live video from the camera (if the activity is active) - if (self.ca.ui.ACTIVE): - self.ca.ui.updateVideoComponents() + def add_recd(self, recd): + self.mediaHashs[recd.type].append(recd) + if self._mode == recd.type: + self.activity.add_thumbnail(recd, True) - self.ca.ui.progressWindow.updateProgress( 0, "" ) - self.setRecording( False ) - self.setUpdating( False ) + if not recd.buddy: + self.collab.share_recd(recd) + # called from gstreamer thread + def save_photo(self, pixbuf): + recd = self.createNewRecorded(constants.TYPE_PHOTO) - def abandonRecording( self ): + imgpath = os.path.join(Instance.instancePath, recd.mediaFilename) + pixbuf.save(imgpath, "jpeg") - self.ca.ui.LAST_MODE = -1 - self.ca.ui.TRANSCODING = False - self.ca.ui.completeTimer() - self.ca.ui.completeCountdown() - self.setRecording(False) + thumbpath = os.path.join(Instance.instancePath, recd.thumbFilename) + pixbuf = utils.generate_thumbnail(pixbuf) + pixbuf.save(thumbpath, "png") - self.ca.ui.progressWindow.updateProgress( 0, "" ) + #now that we've saved both the image and its pixbuf, we get their md5s + self.createNewRecordedMd5Sums( recd ) - self.ca.glive.abandonMedia() + 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)) - def stoppedRecordingVideo( self ): - self.setUpdating( False ) + thumb_path = os.path.join(Instance.instancePath, recd.thumbFilename) + still = utils.generate_thumbnail(still) + still.save(thumb_path, "png") + self.createNewRecordedMd5Sums( recd ) - def startTakingPhoto( self ): - self.setUpdating( True ) - self.ca.glive.takePhoto() + 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)) - def savePhoto( self, pixbuf ): - recd = self.createNewRecorded( Constants.TYPE_PHOTO ) + 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) - imgpath = os.path.join(Instance.instancePath, recd.mediaFilename) - pixbuf.save( imgpath, "jpeg" ) + thumb_path = os.path.join(Instance.instancePath, recd.thumbFilename) + still = utils.generate_thumbnail(still) + still.save(thumb_path, "png") - thumbpath = os.path.join(Instance.instancePath, recd.thumbFilename) - scale = float((UI.dim_THUMB_WIDTH+0.0)/(pixbuf.get_width()+0.0)) - thumbImg = utils.generateThumbnail(pixbuf, scale, UI.dim_THUMB_WIDTH, UI.dim_THUMB_HEIGHT) - thumbImg.write_to_png(thumbpath) - gc.collect() - #now that we've saved both the image and its pixbuf, we get their md5s self.createNewRecordedMd5Sums( recd ) - photoHash = self.mediaHashs[Constants.TYPE_PHOTO] - photoHash.append( recd ) - gobject.idle_add(self.displayThumb, recd, True) + 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) - self.meshShareRecd( recd ) + def start_seek(self): + self.gplay.pause() + def do_seek(self, position): + self.gplay.seek(position) - def addMeshRecd( self, recd ): - #todo: sort on time-taken, not on their arrival time over the mesh (?) - self.mediaHashs[recd.type].append( recd ) + def end_seek(self): + self.gplay.play() - #updateUi, but don't lock up the buttons if they're recording or whatever - gobject.idle_add(self.displayThumb, recd, False) + 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( ) + def createNewRecorded(self, type): + recd = Recorded() - recd.recorderName = Instance.nickName + recd.recorderName = self.get_nickname() recd.recorderHash = Instance.keyHashPrintable #to create a file, use the hardware_id+time *and* check if available or not @@ -367,7 +338,7 @@ class Model: mediaThumbFilename = str(recd.recorderHash) + "_" + str(recd.time) mediaFilename = mediaThumbFilename - mediaFilename = mediaFilename + "." + Constants.mediaTypes[type][Constants.keyExt] + 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 ) @@ -377,16 +348,18 @@ class Model: thumbFilepath = utils.getUniqueFilepath( thumbFilepath, 0 ) recd.thumbFilename = os.path.basename( thumbFilepath ) - stringType = Constants.mediaTypes[type][Constants.keyIstr] - recd.title = Constants.istrBy % {"1":stringType, "2":str(recd.recorderName)} + stringType = constants.MEDIA_INFO[type]['istr'] + + # Translators: photo by photographer, e.g. "Photo by Mary" + recd.title = _('%s by %s') % (stringType, recd.recorderName) - recd.colorStroke = Instance.colorStroke - recd.colorFill = Instance.colorFill + color = sugar.profile.get_color() + recd.colorStroke = color.get_stroke_color() + recd.colorFill = color.get_fill_color() logger.debug('createNewRecorded: ' + str(recd) + ", thumbFilename:" + str(recd.thumbFilename)) return recd - def createNewRecordedMd5Sums( self, recd ): recd.thumbMd5 = recd.mediaMd5 = str(uuid.uuid4()) @@ -402,56 +375,29 @@ class Model: mBytes = os.stat(mediaFile)[6] recd.mediaBytes = mBytes - - def deleteRecorded( self, recd ): + def delete_recd(self, recd): recd.deleted = True + self.mediaHashs[recd.type].remove(recd) - #clear the index - hash = self.mediaHashs[recd.type] - index = hash.index(recd) - hash.remove( recd ) - - if (not recd.meshUploading): - self.doDeleteRecorded( recd ) - + if recd.meshUploading: + return - def doDeleteRecorded( self, recd ): #remove files from the filesystem if not on the datastore - if (recd.datastoreId == None): + if recd.datastoreId == None: mediaFile = recd.getMediaFilepath() - if (os.path.exists(mediaFile)): + if os.path.exists(mediaFile): os.remove(mediaFile) - thumbFile = recd.getThumbFilepath( ) - if (os.path.exists(thumbFile)): + thumbFile = recd.getThumbFilepath() + if os.path.exists(thumbFile): os.remove(thumbFile) else: #remove from the datastore here, since once gone, it is gone... - serialize.removeMediaFromDatastore( recd ) - - - def doVideoMode( self ): - if (self.MODE == Constants.MODE_VIDEO): - return + serialize.removeMediaFromDatastore(recd) - self.MODE = Constants.MODE_VIDEO - self.setUpdating(True) - gobject.idle_add( self.setupMode, self.MODE, True ) - - - def doPhotoMode( self ): - if (self.MODE == Constants.MODE_PHOTO): - return - - self.MODE = Constants.MODE_PHOTO - self.setUpdating(True) - gobject.idle_add( self.setupMode, self.MODE, True ) - - - def doAudioMode( self ): - if (self.MODE == Constants.MODE_AUDIO): - return + 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) - self.MODE = Constants.MODE_AUDIO - self.setUpdating(True) - gobject.idle_add( self.setupMode, self.MODE, True ) diff --git a/network.py b/network.py new file mode 100644 index 0000000..bd4848a --- /dev/null +++ b/network.py @@ -0,0 +1,1076 @@ +# +# Copyright (C) 2008-2010 One Laptop Per Child +# Copyright (C) 2009 Tomeu Vizoso, Simon Schampijer +# Copyright (C) 2009 Paraguay Educa, Martin Abente +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gettext import gettext as _ +import logging +import sha +import socket +import struct +import re +import datetime +import time +import gtk +import gobject +import gconf +import dbus + +from sugar.graphics.icon import get_icon_state +from sugar.graphics import style +from sugar.graphics.palette import Palette +from sugar.graphics.toolbutton import ToolButton +from sugar.graphics.tray import TrayIcon +from sugar.graphics import xocolor +from sugar.util import unique_id +from sugar import profile + +from jarabe.model import network +from jarabe.model.network import Settings +from jarabe.model.network import IP4Config +from jarabe.frame.frameinvoker import FrameWidgetInvoker +from jarabe.view.pulsingicon import PulsingIcon + +IP_ADDRESS_TEXT_TEMPLATE = _("IP address: %s") + +_NM_SERVICE = 'org.freedesktop.NetworkManager' +_NM_IFACE = 'org.freedesktop.NetworkManager' +_NM_PATH = '/org/freedesktop/NetworkManager' +_NM_DEVICE_IFACE = 'org.freedesktop.NetworkManager.Device' +_NM_WIRED_IFACE = 'org.freedesktop.NetworkManager.Device.Wired' +_NM_WIRELESS_IFACE = 'org.freedesktop.NetworkManager.Device.Wireless' +_NM_OLPC_MESH_IFACE = 'org.freedesktop.NetworkManager.Device.OlpcMesh' +_NM_SERIAL_IFACE = 'org.freedesktop.NetworkManager.Device.Serial' +_NM_ACCESSPOINT_IFACE = 'org.freedesktop.NetworkManager.AccessPoint' +_NM_ACTIVE_CONN_IFACE = 'org.freedesktop.NetworkManager.Connection.Active' + +_GSM_STATE_NOT_READY = 0 +_GSM_STATE_DISCONNECTED = 1 +_GSM_STATE_CONNECTING = 2 +_GSM_STATE_CONNECTED = 3 +_GSM_STATE_NEED_AUTH = 4 + +def frequency_to_channel(frequency): + ftoc = { 2412: 1, 2417: 2, 2422: 3, 2427: 4, + 2432: 5, 2437: 6, 2442: 7, 2447: 8, + 2452: 9, 2457: 10, 2462: 11, 2467: 12, + 2472: 13} + return ftoc[frequency] + +class WirelessPalette(Palette): + __gtype_name__ = 'SugarWirelessPalette' + + __gsignals__ = { + 'deactivate-connection' : (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])), + 'create-connection' : (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])), + } + + def __init__(self, primary_text, can_create=True): + Palette.__init__(self, label=primary_text) + + self._disconnect_item = None + + self._channel_label = gtk.Label() + self._channel_label.props.xalign = 0.0 + self._channel_label.show() + + self._ip_address_label = gtk.Label() + + self._info = gtk.VBox() + + def _padded(child, xalign=0, yalign=0.5): + padder = gtk.Alignment(xalign=xalign, yalign=yalign, + xscale=1, yscale=0.33) + padder.set_padding(style.DEFAULT_SPACING, + style.DEFAULT_SPACING, + style.DEFAULT_SPACING, + style.DEFAULT_SPACING) + padder.add(child) + return padder + + self._info.pack_start(_padded(self._channel_label)) + self._info.pack_start(_padded(self._ip_address_label)) + self._info.show_all() + + self._disconnect_item = gtk.MenuItem(_('Disconnect...')) + self._disconnect_item.connect('activate', self.__disconnect_activate_cb) + self.menu.append(self._disconnect_item) + + if can_create: + self._adhoc_item = gtk.MenuItem(_('Create new wireless network')) + self._adhoc_item.connect('activate', self.__adhoc_activate_cb) + self.menu.append(self._adhoc_item) + self._adhoc_item.show() + + def set_connecting(self): + self.props.secondary_text = _('Connecting...') + + def _set_connected(self, iaddress): + self.set_content(self._info) + self.props.secondary_text = _('Connected') + self._set_ip_address(iaddress) + self._disconnect_item.show() + + def set_connected_with_frequency(self, frequency, iaddress): + self._set_connected(iaddress) + self._set_frequency(frequency) + + def set_connected_with_channel(self, channel, iaddress): + self._set_connected(iaddress) + self._set_channel(channel) + + def set_disconnected(self): + self.props.primary_text = '' + self.props.secondary_text = '' + self._disconnect_item.hide() + self.set_content(None) + + def __disconnect_activate_cb(self, menuitem): + self.emit('deactivate-connection') + + def __adhoc_activate_cb(self, menuitem): + self.emit('create-connection') + + def _set_frequency(self, frequency): + try: + channel = frequency_to_channel(frequency) + except KeyError: + channel = 0 + self._set_channel(channel) + + def _set_channel(self, channel): + self._channel_label.set_text("%s: %d" % (_("Channel"), channel)) + + def _set_ip_address(self, ip_address): + if ip_address is not None: + ip_address_text = IP_ADDRESS_TEXT_TEMPLATE % \ + socket.inet_ntoa(struct.pack('I', ip_address)) + else: + ip_address_text = "" + self._ip_address_label.set_text(ip_address_text) + + +class WiredPalette(Palette): + __gtype_name__ = 'SugarWiredPalette' + + def __init__(self): + Palette.__init__(self, label=_('Wired Network')) + + self._speed_label = gtk.Label() + self._speed_label.props.xalign = 0.0 + self._speed_label.show() + + self._ip_address_label = gtk.Label() + + self._info = gtk.VBox() + + def _padded(child, xalign=0, yalign=0.5): + padder = gtk.Alignment(xalign=xalign, yalign=yalign, + xscale=1, yscale=0.33) + padder.set_padding(style.DEFAULT_SPACING, + style.DEFAULT_SPACING, + style.DEFAULT_SPACING, + style.DEFAULT_SPACING) + padder.add(child) + return padder + + self._info.pack_start(_padded(self._speed_label)) + self._info.pack_start(_padded(self._ip_address_label)) + self._info.show_all() + + self.set_content(self._info) + self.props.secondary_text = _('Connected') + + def set_connected(self, speed, iaddress): + self._speed_label.set_text('%s: %d Mb/s' % (_('Speed'), speed)) + self._set_ip_address(iaddress) + + def _inet_ntoa(self, iaddress): + address = ['%s' % ((iaddress >> i) % 256) for i in [0, 8, 16, 24]] + return ".".join(address) + + def _set_ip_address(self, ip_address): + if ip_address is not None: + ip_address_text = IP_ADDRESS_TEXT_TEMPLATE % \ + socket.inet_ntoa(struct.pack('I', ip_address)) + else: + ip_address_text = "" + self._ip_address_label.set_text(ip_address_text) + +class GsmPalette(Palette): + __gtype_name__ = 'SugarGsmPalette' + + __gsignals__ = { + 'gsm-connect' : (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])), + 'gsm-disconnect' : (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([])), + } + + def __init__(self): + Palette.__init__(self, label=_('Wireless modem')) + + self._current_state = None + + self._toggle_state_item = gtk.MenuItem('') + self._toggle_state_item.connect('activate', self.__toggle_state_cb) + self.menu.append(self._toggle_state_item) + self._toggle_state_item.show() + + self.set_gsm_state(_GSM_STATE_NOT_READY) + + self.info_box = gtk.VBox() + + self.data_label = gtk.Label() + self.data_label.props.xalign = 0.0 + label_alignment = self._add_widget_with_padding(self.data_label) + self.info_box.pack_start(label_alignment) + self.data_label.show() + label_alignment.show() + + self.connection_time_label = gtk.Label() + self.connection_time_label.props.xalign = 0.0 + label_alignment = self._add_widget_with_padding( \ + self.connection_time_label) + self.info_box.pack_start(label_alignment) + self.connection_time_label.show() + label_alignment.show() + + self.info_box.show() + self.set_content(self.info_box) + + def _add_widget_with_padding(self, child, xalign=0, yalign=0.5): + alignment = gtk.Alignment(xalign=xalign, yalign=yalign, + xscale=1, yscale=0.33) + alignment.set_padding(style.DEFAULT_SPACING, + style.DEFAULT_SPACING, + style.DEFAULT_SPACING, + style.DEFAULT_SPACING) + alignment.add(child) + return alignment + + def set_gsm_state(self, state): + self._current_state = state + self._update_label_and_text() + + def _update_label_and_text(self): + if self._current_state == _GSM_STATE_NOT_READY: + self._toggle_state_item.get_child().set_label('...') + self.props.secondary_text = _('Please wait...') + + elif self._current_state == _GSM_STATE_DISCONNECTED: + self._toggle_state_item.get_child().set_label(_('Connect')) + self.props.secondary_text = _('Disconnected') + + elif self._current_state == _GSM_STATE_CONNECTING: + self._toggle_state_item.get_child().set_label(_('Cancel')) + self.props.secondary_text = _('Connecting...') + + elif self._current_state == _GSM_STATE_CONNECTED: + self._toggle_state_item.get_child().set_label(_('Disconnect')) + self.props.secondary_text = _('Connected') + + elif self._current_state == _GSM_STATE_NEED_AUTH: + self._toggle_state_item.get_child().set_label(_('Sim requires Pin/Puk')) + self.props.secondary_text = _('Authentication Error') + + else: + raise ValueError('Invalid GSM state while updating label and ' \ + 'text, %s' % str(self._current_state)) + + def __toggle_state_cb(self, menuitem): + if self._current_state == _GSM_STATE_NOT_READY: + pass + elif self._current_state == _GSM_STATE_DISCONNECTED: + self.emit('gsm-connect') + elif self._current_state == _GSM_STATE_CONNECTING: + self.emit('gsm-disconnect') + elif self._current_state == _GSM_STATE_CONNECTED: + self.emit('gsm-disconnect') + elif self._current_state == _GSM_STATE_NEED_AUTH: + self.emit('gsm-disconnect') + else: + raise ValueError('Invalid GSM state while emitting signal, %s' % \ + str(self._current_state)) + + +class WirelessDeviceView(ToolButton): + + _ICON_NAME = 'network-wireless' + FRAME_POSITION_RELATIVE = 302 + + def __init__(self, device): + ToolButton.__init__(self) + + self._bus = dbus.SystemBus() + self._device = device + self._device_props = None + self._flags = 0 + self._name = '' + self._mode = network.NM_802_11_MODE_UNKNOWN + self._strength = 0 + self._frequency = 0 + self._device_state = None + self._color = None + self._active_ap_op = None + + self._icon = PulsingIcon() + self._icon.props.icon_name = get_icon_state(self._ICON_NAME, 0) + self._inactive_color = xocolor.XoColor( \ + "%s,%s" % (style.COLOR_BUTTON_GREY.get_svg(), + style.COLOR_TRANSPARENT.get_svg())) + self._icon.props.pulse_color = self._inactive_color + self._icon.props.base_color = self._inactive_color + + self.set_icon_widget(self._icon) + self._icon.show() + + self.set_palette_invoker(FrameWidgetInvoker(self)) + self._palette = WirelessPalette(self._name) + self._palette.connect('deactivate-connection', + self.__deactivate_connection_cb) + self._palette.connect('create-connection', + self.__create_connection_cb) + self.set_palette(self._palette) + self._palette.set_group_id('frame') + + self._device_props = dbus.Interface(self._device, dbus.PROPERTIES_IFACE) + self._device_props.GetAll(_NM_DEVICE_IFACE, byte_arrays=True, + reply_handler=self.__get_device_props_reply_cb, + error_handler=self.__get_device_props_error_cb) + + self._device_props.Get(_NM_WIRELESS_IFACE, 'ActiveAccessPoint', + reply_handler=self.__get_active_ap_reply_cb, + error_handler=self.__get_active_ap_error_cb) + + self._bus.add_signal_receiver(self.__state_changed_cb, + signal_name='StateChanged', + path=self._device.object_path, + dbus_interface=_NM_DEVICE_IFACE) + + def disconnect(self): + self._bus.remove_signal_receiver(self.__state_changed_cb, + signal_name='StateChanged', + path=self._device.object_path, + dbus_interface=_NM_DEVICE_IFACE) + + def __get_device_props_reply_cb(self, properties): + if 'State' in properties: + self._device_state = properties['State'] + self._update_state() + + def __get_device_props_error_cb(self, err): + logging.error('Error getting the device properties: %s', err) + + def __get_active_ap_reply_cb(self, active_ap_op): + if self._active_ap_op != active_ap_op: + if self._active_ap_op is not None: + self._bus.remove_signal_receiver( + self.__ap_properties_changed_cb, + signal_name='PropertiesChanged', + path=self._active_ap_op, + dbus_interface=_NM_ACCESSPOINT_IFACE) + if active_ap_op == '/': + self._active_ap_op = None + return + self._active_ap_op = active_ap_op + active_ap = self._bus.get_object(_NM_SERVICE, active_ap_op) + props = dbus.Interface(active_ap, dbus.PROPERTIES_IFACE) + + props.GetAll(_NM_ACCESSPOINT_IFACE, byte_arrays=True, + reply_handler=self.__get_all_ap_props_reply_cb, + error_handler=self.__get_all_ap_props_error_cb) + + self._bus.add_signal_receiver(self.__ap_properties_changed_cb, + signal_name='PropertiesChanged', + path=self._active_ap_op, + dbus_interface=_NM_ACCESSPOINT_IFACE) + + def __get_active_ap_error_cb(self, err): + logging.error('Error getting the active access point: %s', err) + + def __state_changed_cb(self, new_state, old_state, reason): + self._device_state = new_state + self._update_state() + self._device_props.Get(_NM_WIRELESS_IFACE, 'ActiveAccessPoint', + reply_handler=self.__get_active_ap_reply_cb, + error_handler=self.__get_active_ap_error_cb) + + def __ap_properties_changed_cb(self, properties): + self._update_properties(properties) + + def _name_encodes_colors(self): + """Match #XXXXXX,#YYYYYY at the end of the network name""" + return self._name[-7] == '#' and self._name[-8] == ',' \ + and self._name[-15] == '#' + + def _update_properties(self, properties): + if 'Mode' in properties: + self._mode = properties['Mode'] + self._color = None + if 'Ssid' in properties: + self._name = properties['Ssid'] + self._color = None + if 'Strength' in properties: + self._strength = properties['Strength'] + if 'Flags' in properties: + self._flags = properties['Flags'] + if 'Frequency' in properties: + self._frequency = properties['Frequency'] + + if self._color == None: + if self._mode == network.NM_802_11_MODE_ADHOC \ + and self._name_encodes_colors(): + encoded_color = self._name.split("#", 1) + if len(encoded_color) == 2: + self._color = xocolor.XoColor('#' + encoded_color[1]) + else: + sh = sha.new() + data = self._name + hex(self._flags) + sh.update(data) + h = hash(sh.digest()) + idx = h % len(xocolor.colors) + + self._color = xocolor.XoColor('%s,%s' % + (xocolor.colors[idx][0], + xocolor.colors[idx][1])) + self._update() + + def __get_all_ap_props_reply_cb(self, properties): + self._update_properties(properties) + + def __get_all_ap_props_error_cb(self, err): + logging.error('Error getting the access point properties: %s', err) + + def _update(self): + if self._flags == network.NM_802_11_AP_FLAGS_PRIVACY: + self._icon.props.badge_name = "emblem-locked" + else: + self._icon.props.badge_name = None + + self._palette.props.primary_text = self._name + + self._update_state() + self._update_color() + + def _update_state(self): + if self._active_ap_op is not None: + state = self._device_state + else: + state = network.DEVICE_STATE_UNKNOWN + + if state == network.DEVICE_STATE_ACTIVATED: + icon_name = '%s-connected' % self._ICON_NAME + else: + icon_name = self._ICON_NAME + + icon_name = get_icon_state(icon_name, self._strength) + if icon_name: + self._icon.props.icon_name = icon_name + + if state == network.DEVICE_STATE_PREPARE or \ + state == network.DEVICE_STATE_CONFIG or \ + state == network.DEVICE_STATE_NEED_AUTH or \ + state == network.DEVICE_STATE_IP_CONFIG: + self._palette.set_connecting() + self._icon.props.pulsing = True + elif state == network.DEVICE_STATE_ACTIVATED: + address = self._device_props.Get(_NM_DEVICE_IFACE, 'Ip4Address') + self._palette.set_connected_with_frequency(self._frequency, + address) + self._icon.props.pulsing = False + else: + self._icon.props.badge_name = None + self._icon.props.pulsing = False + self._icon.props.pulse_color = self._inactive_color + self._icon.props.base_color = self._inactive_color + self._palette.set_disconnected() + + def _update_color(self): + self._icon.props.base_color = self._color + + def __deactivate_connection_cb(self, palette, data=None): + connection = network.find_connection_by_ssid(self._name) + if connection: + if self._mode == network.NM_802_11_MODE_INFRA: + connection.set_disconnected() + + if self._active_ap_op is not None: + obj = self._bus.get_object(_NM_SERVICE, _NM_PATH) + netmgr = dbus.Interface(obj, _NM_IFACE) + netmgr_props = dbus.Interface(netmgr, dbus.PROPERTIES_IFACE) + active_connections_o = netmgr_props.Get(_NM_IFACE, + 'ActiveConnections') + + for conn_o in active_connections_o: + obj = self._bus.get_object(_NM_IFACE, conn_o) + props = dbus.Interface(obj, dbus.PROPERTIES_IFACE) + ap_op = props.Get(_NM_ACTIVE_CONN_IFACE, 'SpecificObject') + if ap_op == self._active_ap_op: + netmgr.DeactivateConnection(conn_o) + break + + def __create_connection_cb(self, palette, data=None): + """Create an 802.11 IBSS network. + + The user's color is encoded at the end of the network name. The network + name is truncated so that it does not exceed the 32 byte SSID limit. + """ + client = gconf.client_get_default() + nick = client.get_string('/desktop/sugar/user/nick').decode('utf-8') + color = client.get_string('/desktop/sugar/user/color') + color_suffix = ' %s' % color + + format = _('%s\'s network').encode('utf-8') + extra_length = (len(format) - len('%s')) + len(color_suffix) + name_limit = 32 - extra_length + + # truncate the nick and use a regex to drop any partial characters + # at the end + nick = nick.encode('utf-8')[:name_limit] + pattern = "([\xf6-\xf7][\x80-\xbf]{0,2}|[\xe0-\xef][\x80-\xbf]{0,1}|[\xc0-\xdf])$" + nick = re.sub(pattern, '', nick) + + connection_name = format % nick + connection_name += color_suffix + + connection = network.find_connection_by_ssid(connection_name) + if connection is None: + settings = Settings() + settings.connection.id = 'Auto ' + connection_name + uuid = settings.connection.uuid = unique_id() + settings.connection.type = '802-11-wireless' + settings.wireless.ssid = dbus.ByteArray(connection_name) + settings.wireless.band = 'bg' + settings.wireless.mode = 'adhoc' + settings.ip4_config = IP4Config() + settings.ip4_config.method = 'link-local' + + connection = network.add_connection(uuid, settings) + + obj = self._bus.get_object(_NM_SERVICE, _NM_PATH) + netmgr = dbus.Interface(obj, _NM_IFACE) + + netmgr.ActivateConnection(network.SETTINGS_SERVICE, + connection.path, + self._device.object_path, + '/', + reply_handler=self.__activate_reply_cb, + error_handler=self.__activate_error_cb) + + def __activate_reply_cb(self, connection): + logging.debug('Network created: %s', connection) + + def __activate_error_cb(self, err): + logging.debug('Failed to create network: %s', err) + +class OlpcMeshDeviceView(ToolButton): + _ICON_NAME = 'network-mesh' + FRAME_POSITION_RELATIVE = 302 + + def __init__(self, device, state): + ToolButton.__init__(self) + + self._bus = dbus.SystemBus() + self._device = device + self._device_props = None + self._device_state = None + self._channel = 0 + + self._icon = PulsingIcon(icon_name=self._ICON_NAME) + self._inactive_color = xocolor.XoColor( \ + "%s,%s" % (style.COLOR_BUTTON_GREY.get_svg(), + style.COLOR_TRANSPARENT.get_svg())) + self._icon.props.pulse_color = profile.get_color() + self._icon.props.base_color = self._inactive_color + + self.set_icon_widget(self._icon) + self._icon.show() + + self.set_palette_invoker(FrameWidgetInvoker(self)) + self._palette = WirelessPalette(_("Mesh Network"), can_create=False) + self._palette.connect('deactivate-connection', + self.__deactivate_connection) + self.set_palette(self._palette) + self._palette.set_group_id('frame') + + self.update_state(state) + + self._device_props = dbus.Interface(self._device, + 'org.freedesktop.DBus.Properties') + self._device_props.Get(_NM_OLPC_MESH_IFACE, 'ActiveChannel', + reply_handler=self.__get_active_channel_reply_cb, + error_handler=self.__get_active_channel_error_cb) + + self._bus.add_signal_receiver(self.__wireless_properties_changed_cb, + signal_name='PropertiesChanged', + path=device.object_path, + dbus_interface=_NM_OLPC_MESH_IFACE) + + def disconnect(self): + self._bus.remove_signal_receiver(self.__wireless_properties_changed_cb, + signal_name='PropertiesChanged', + path=self._device.object_path, + dbus_interface=_NM_OLPC_MESH_IFACE) + + def __get_active_channel_reply_cb(self, channel): + self._channel = channel + self._update_text() + + def __get_active_channel_error_cb(self, err): + logging.error('Error getting the active channel: %s', err) + + def __state_changed_cb(self, new_state, old_state, reason): + self._device_state = new_state + self._update() + + def __wireless_properties_changed_cb(self, properties): + if 'ActiveChannel' in properties: + self._channel = properties['ActiveChannel'] + self._update_text() + + def _update_text(self): + state = self._device_state + if state in (network.DEVICE_STATE_PREPARE, network.DEVICE_STATE_CONFIG, + network.DEVICE_STATE_NEED_AUTH, + network.DEVICE_STATE_IP_CONFIG, + network.DEVICE_STATE_ACTIVATED): + text = _("Mesh Network") + " " + str(self._channel) + else: + text = _("Mesh Network") + self._palette.props.primary_text = text + + def _update(self): + state = self._device_state + + if state in [network.DEVICE_STATE_PREPARE, + network.DEVICE_STATE_CONFIG, + network.DEVICE_STATE_NEED_AUTH, + network.DEVICE_STATE_IP_CONFIG]: + self._icon.props.base_color = self._inactive_color + self._icon.props.pulse_color = profile.get_color() + self._palette.set_connecting() + self._icon.props.pulsing = True + elif state == network.DEVICE_STATE_ACTIVATED: + address = self._device_props.Get(_NM_DEVICE_IFACE, 'Ip4Address') + self._palette.set_connected_with_channel(self._channel, address) + self._icon.props.base_color = profile.get_color() + self._icon.props.pulsing = False + self._update_text() + + def update_state(self, state): + self._device_state = state + self._update() + + def __deactivate_connection(self, palette, data=None): + obj = self._bus.get_object(_NM_SERVICE, _NM_PATH) + netmgr = dbus.Interface(obj, _NM_IFACE) + netmgr_props = dbus.Interface(netmgr, 'org.freedesktop.DBus.Properties') + active_connections_o = netmgr_props.Get(_NM_IFACE, + 'ActiveConnections') + + for conn_o in active_connections_o: + # The connection path for a mesh connection is the device itself. + obj = self._bus.get_object(_NM_IFACE, conn_o) + props = dbus.Interface(obj, 'org.freedesktop.DBus.Properties') + ap_op = props.Get(_NM_ACTIVE_CONN_IFACE, 'SpecificObject') + + try: + obj = self._bus.get_object(_NM_IFACE, ap_op) + props = dbus.Interface(obj, 'org.freedesktop.DBus.Properties') + type = props.Get(_NM_DEVICE_IFACE, 'DeviceType') + if type == network.DEVICE_TYPE_802_11_OLPC_MESH: + netmgr.DeactivateConnection(conn_o) + break + except dbus.exceptions.DBusException: + pass + +class WiredDeviceView(TrayIcon): + + _ICON_NAME = 'network-wired' + FRAME_POSITION_RELATIVE = 301 + + def __init__(self, speed, address): + client = gconf.client_get_default() + color = xocolor.XoColor(client.get_string('/desktop/sugar/user/color')) + + TrayIcon.__init__(self, icon_name=self._ICON_NAME, xo_color=color) + + self.set_palette_invoker(FrameWidgetInvoker(self)) + self._palette = WiredPalette() + self.set_palette(self._palette) + self._palette.set_group_id('frame') + self._palette.set_connected(speed, address) + + +class GsmDeviceView(TrayIcon): + + _ICON_NAME = 'network-gsm' + FRAME_POSITION_RELATIVE = 303 + + def __init__(self, device): + self._connection_time_handler = None + self._connection_timestamp = 0 + + client = gconf.client_get_default() + color = xocolor.XoColor(client.get_string('/desktop/sugar/user/color')) + + TrayIcon.__init__(self, icon_name=self._ICON_NAME, xo_color=color) + + self._bus = dbus.SystemBus() + self._device = device + + self.set_palette_invoker(FrameWidgetInvoker(self)) + self._palette = self._create_gsm_palette() + self.set_palette(self._palette) + + self._bus.add_signal_receiver(self.__state_changed_cb, + signal_name='StateChanged', + path=self._device.object_path, + dbus_interface=_NM_DEVICE_IFACE) + self._bus.add_signal_receiver(self.__ppp_stats_changed_cb, + signal_name='PppStats', + path=self._device.object_path, + dbus_interface=_NM_SERIAL_IFACE) + + def _create_gsm_palette(self): + palette = GsmPalette() + + palette.set_group_id('frame') + palette.connect('gsm-connect', self.__gsm_connect_cb) + palette.connect('gsm-disconnect', self.__gsm_disconnect_cb) + + props = dbus.Interface(self._device, 'org.freedesktop.DBus.Properties') + props.GetAll(_NM_DEVICE_IFACE, byte_arrays=True, + reply_handler=self.__current_state_check_cb, + error_handler=self.__current_state_check_error_cb) + + return palette + + def __gsm_connect_cb(self, palette, data=None): + connection = network.find_gsm_connection() + if connection is not None: + obj = self._bus.get_object(_NM_SERVICE, _NM_PATH) + netmgr = dbus.Interface(obj, _NM_IFACE) + netmgr.ActivateConnection(network.SETTINGS_SERVICE, + connection.path, + self._device.object_path, + '/', + reply_handler=self.__connect_cb, + error_handler=self.__connect_error_cb) + + def __connect_cb(self, active_connection): + logging.debug('Connected successfully to gsm device, %s', + active_connection) + + def __connect_error_cb(self, error): + raise RuntimeError('Error when connecting to gsm device, %s' % error) + + def __gsm_disconnect_cb(self, palette, data=None): + obj = self._bus.get_object(_NM_SERVICE, _NM_PATH) + netmgr = dbus.Interface(obj, _NM_IFACE) + netmgr_props = dbus.Interface(netmgr, 'org.freedesktop.DBus.Properties') + active_connections_o = netmgr_props.Get(_NM_IFACE, 'ActiveConnections') + + for conn_o in active_connections_o: + obj = self._bus.get_object(_NM_IFACE, conn_o) + props = dbus.Interface(obj, 'org.freedesktop.DBus.Properties') + devices = props.Get(_NM_ACTIVE_CONN_IFACE, 'Devices') + if self._device.object_path in devices: + netmgr.DeactivateConnection( + conn_o, + reply_handler=self.__disconnect_cb, + error_handler=self.__disconnect_error_cb) + break + + def __disconnect_cb(self): + logging.debug('Disconnected successfully gsm device') + + def __disconnect_error_cb(self, error): + raise RuntimeError('Error when disconnecting gsm device, %s' % error) + + def __state_changed_cb(self, new_state, old_state, reason): + logging.debug('GSM State: %s to %s, reason %s', old_state, new_state, reason) + self._update_state(int(new_state)) + + def __current_state_check_cb(self, properties): + self._update_state(int(properties['State'])) + + def __current_state_check_error_cb(self, error): + raise RuntimeError('Error when checking gsm device state, %s' % error) + + def _update_state(self, state): + gsm_state = None + + if state == network.DEVICE_STATE_ACTIVATED: + gsm_state = _GSM_STATE_CONNECTED + connection = network.find_gsm_connection() + if connection is not None: + connection.set_connected() + self._connection_timestamp = time.time() - \ + connection.get_settings().connection.timestamp + self._connection_time_handler = gobject.timeout_add( \ + 1000, self.__connection_timecount_cb) + self._update_stats(0, 0) + self._update_connection_time() + self._palette.info_box.show() + + if state == network.DEVICE_STATE_DISCONNECTED: + gsm_state = _GSM_STATE_DISCONNECTED + self._connection_timestamp = 0 + if self._connection_time_handler is not None: + gobject.source_remove(self._connection_time_handler) + self._palette.info_box.hide() + + elif state in [network.DEVICE_STATE_UNMANAGED, + network.DEVICE_STATE_UNAVAILABLE, + network.DEVICE_STATE_UNKNOWN]: + gsm_state = _GSM_STATE_NOT_READY + + elif state in [network.DEVICE_STATE_PREPARE, + network.DEVICE_STATE_CONFIG, + network.DEVICE_STATE_IP_CONFIG]: + gsm_state = _GSM_STATE_CONNECTING + + elif state in [network.DEVICE_STATE_NEED_AUTH]: + gsm_state = _GSM_STATE_NEED_AUTH + + if self._palette is not None: + self._palette.set_gsm_state(gsm_state) + + def disconnect(self): + self._bus.remove_signal_receiver(self.__state_changed_cb, + signal_name='StateChanged', + path=self._device.object_path, + dbus_interface=_NM_DEVICE_IFACE) + + def __ppp_stats_changed_cb(self, in_bytes, out_bytes): + self._update_stats(in_bytes, out_bytes) + + def _update_stats(self, in_bytes, out_bytes): + in_kbytes = in_bytes / 1024 + out_kbytes = out_bytes / 1024 + text = _("Data sent %d kb / received %d kb") % (out_kbytes, in_kbytes) + self._palette.data_label.set_text(text) + + def __connection_timecount_cb(self): + self._connection_timestamp = self._connection_timestamp + 1 + self._update_connection_time() + return True + + def _update_connection_time(self): + connection_time = datetime.datetime.fromtimestamp( \ + self._connection_timestamp) + text = _("Connection time ") + connection_time.strftime('%H : %M : %S') + self._palette.connection_time_label.set_text(text) + +class WirelessDeviceObserver(object): + def __init__(self, device, tray): + self._device = device + self._device_view = None + self._tray = tray + self._device_view = WirelessDeviceView(self._device) + self._tray.add_device(self._device_view) + + def disconnect(self): + self._device_view.disconnect() + self._tray.remove_device(self._device_view) + del self._device_view + self._device_view = None + + +class MeshDeviceObserver(object): + def __init__(self, device, tray): + self._bus = dbus.SystemBus() + self._device = device + self._device_view = None + self._tray = tray + + props = dbus.Interface(self._device, dbus.PROPERTIES_IFACE) + props.GetAll(_NM_DEVICE_IFACE, byte_arrays=True, + reply_handler=self.__get_device_props_reply_cb, + error_handler=self.__get_device_props_error_cb) + + self._bus.add_signal_receiver(self.__state_changed_cb, + signal_name='StateChanged', + path=self._device.object_path, + dbus_interface=_NM_DEVICE_IFACE) + + def _remove_device_view(self): + self._device_view.disconnect() + self._tray.remove_device(self._device_view) + del self._device_view + self._device_view = None + + + + def disconnect(self): + if self._device_view is not None: + self._remove_device_view() + + self._bus.remove_signal_receiver(self.__state_changed_cb, + signal_name='StateChanged', + path=self._device.object_path, + dbus_interface=_NM_DEVICE_IFACE) + + def __get_device_props_reply_cb(self, properties): + if 'State' in properties: + self._update_state(properties['State']) + + def __get_device_props_error_cb(self, err): + logging.error('Error getting the device properties: %s', err) + + def __state_changed_cb(self, new_state, old_state, reason): + self._update_state(new_state) + + def _update_state(self, state): + if state >= network.DEVICE_STATE_PREPARE \ + and state <= network.DEVICE_STATE_ACTIVATED: + if self._device_view is not None: + self._device_view.update_state(state) + return + + self._device_view = OlpcMeshDeviceView(self._device, state) + self._tray.add_device(self._device_view) + else: + if self._device_view is not None: + self._remove_device_view() + + +class WiredDeviceObserver(object): + def __init__(self, device, tray): + self._bus = dbus.SystemBus() + self._device = device + self._device_state = None + self._device_view = None + self._tray = tray + + props = dbus.Interface(self._device, dbus.PROPERTIES_IFACE) + props.GetAll(_NM_DEVICE_IFACE, byte_arrays=True, + reply_handler=self.__get_device_props_reply_cb, + error_handler=self.__get_device_props_error_cb) + + self._bus.add_signal_receiver(self.__state_changed_cb, + signal_name='StateChanged', + path=self._device.object_path, + dbus_interface=_NM_DEVICE_IFACE) + + def disconnect(self): + self._bus.remove_signal_receiver(self.__state_changed_cb, + signal_name='StateChanged', + path=self._device.object_path, + dbus_interface=_NM_DEVICE_IFACE) + + def __get_device_props_reply_cb(self, properties): + if 'State' in properties: + self._update_state(properties['State']) + + def __get_device_props_error_cb(self, err): + logging.error('Error getting the device properties: %s', err) + + def __state_changed_cb(self, new_state, old_state, reason): + self._update_state(new_state) + + def _update_state(self, state): + if state == network.DEVICE_STATE_ACTIVATED: + props = dbus.Interface(self._device, dbus.PROPERTIES_IFACE) + address = props.Get(_NM_DEVICE_IFACE, 'Ip4Address') + speed = props.Get(_NM_WIRED_IFACE, 'Speed') + self._device_view = WiredDeviceView(speed, address) + self._tray.add_device(self._device_view) + else: + if self._device_view is not None: + self._tray.remove_device(self._device_view) + del self._device_view + self._device_view = None + +class GsmDeviceObserver(object): + def __init__(self, device, tray): + self._device = device + self._device_view = None + self._tray = tray + + self._device_view = GsmDeviceView(device) + self._tray.add_device(self._device_view) + + def disconnect(self): + self._device_view.disconnect() + self._tray.remove_device(self._device_view) + self._device_view = None + +class NetworkManagerObserver(object): + def __init__(self, tray): + self._bus = dbus.SystemBus() + self._devices = {} + self._netmgr = None + self._tray = tray + + try: + obj = self._bus.get_object(_NM_SERVICE, _NM_PATH) + self._netmgr = dbus.Interface(obj, _NM_IFACE) + except dbus.DBusException: + logging.error('%s service not available', _NM_SERVICE) + return + + self._netmgr.GetDevices(reply_handler=self.__get_devices_reply_cb, + error_handler=self.__get_devices_error_cb) + + self._bus.add_signal_receiver(self.__device_added_cb, + signal_name='DeviceAdded', + dbus_interface=_NM_IFACE) + self._bus.add_signal_receiver(self.__device_removed_cb, + signal_name='DeviceRemoved', + dbus_interface=_NM_IFACE) + + def __get_devices_reply_cb(self, devices): + for device_op in devices: + self._check_device(device_op) + + def __get_devices_error_cb(self, err): + logging.error('Failed to get devices: %s', err) + + def _check_device(self, device_op): + nm_device = self._bus.get_object(_NM_SERVICE, device_op) + props = dbus.Interface(nm_device, dbus.PROPERTIES_IFACE) + + device_type = props.Get(_NM_DEVICE_IFACE, 'DeviceType') + if device_type == network.DEVICE_TYPE_802_3_ETHERNET: + device = WiredDeviceObserver(nm_device, self._tray) + self._devices[device_op] = device + elif device_type == network.DEVICE_TYPE_802_11_WIRELESS: + device = WirelessDeviceObserver(nm_device, self._tray) + self._devices[device_op] = device + elif device_type == network.DEVICE_TYPE_802_11_OLPC_MESH: + device = MeshDeviceObserver(nm_device, self._tray) + self._devices[device_op] = device + elif device_type == network.DEVICE_TYPE_GSM_MODEM: + device = GsmDeviceObserver(nm_device, self._tray) + self._devices[device_op] = device + + def __device_added_cb(self, device_op): + self._check_device(device_op) + + def __device_removed_cb(self, device_op): + if device_op in self._devices: + device = self._devices[device_op] + device.disconnect() + del self._devices[device_op] + +def setup(tray): + device_observer = NetworkManagerObserver(tray) @@ -1,190 +0,0 @@ -#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 gtk -from gtk import gdk -import gobject -import cairo -import math - -#Sometimes a gtk.Image is a useful alternative to a drawing area. You can put a gtk.gdk.Pixmap in the gtk.Image and draw to the gtk.gdk.Pixmap, calling the gtk.Widget.queue_draw() method on the gtk.Image when you want to refresh to the screen. - -class P5(gtk.DrawingArea): - def __init__(self): - super(P5, self).__init__() - - # gtk.Widget signals - self.connect("expose_event", self.expose) - self.connect("button_press_event", self.button_press) - self.connect("button_release_event", self.button_release) - self.connect("motion_notify_event", self.motion_notify) - - # ok, we have to listen to mouse events here too - self.add_events(gdk.BUTTON_PRESS_MASK | gdk.BUTTON_RELEASE_MASK | gdk.POINTER_MOTION_MASK) - self._dragging = False - self._mouseX = 0 - self._mouseY = 0 - - self._w = -1 - self._h = -1 - - - #ok, this calls an initial painting & setting of painterly variables - #e.g. time for the clock widget - #(but not through to redraw_canvas when called here first time) - self._msecUpdate = 100 - self._looping = False - self.noloop() - - - def loop(self): - if (self._looping): - return - else: - self._looping = True - # this is our maybe-threaded refresh (in millisecs) - gobject.timeout_add( self._msecUpdate, self.update ) - - - def noloop(self): - self._looping = False - self.redraw() - - - def redraw(self): - self.update() - - - def expose(self, widget, event): - ctx = widget.window.cairo_create() - - # set a clip region for the expose event - ctx.rectangle(event.area.x, event.area.y, event.area.width, event.area.height) - ctx.clip() - - rect = widget.allocation - #self.draw(ctx, event.area.width, event.area.height) - self.draw( ctx, rect.width, rect.height ) - - - def button_press(self, widget, event): - self._mouseX = event.x - self._mouseY = event.y - self._dragging = True - - - def button_release(self, widget, event): - if self._dragging: - self._dragging = False - - - def motion_notify(self, widget, event): - self._mouseX = event.x - self._mouseY = event.y - - - def draw(self, ctx, w, h): - ctx.set_antialias( cairo.ANTIALIAS_NONE ) - ctx.set_line_width( 1 ) - ctx.identity_matrix( ) - if ((w != self._w) or (h != self._h)): - self._w = w - self._h = h - self.doResize( ) - - - def doResize(self): - pass - - - #called from update - def redraw_canvas(self): - if self.window: - alloc = self.get_allocation() - #this is odd behavior, but once we add this widget to a parent (vbox) - #it requires setting the q_d_a x,y to 0, 0 - #self.queue_draw_area(alloc.x, alloc.y, alloc.width, alloc.height) - self.queue_draw_area(0, 0, alloc.width, alloc.height) - self.window.process_updates(True) - - - def update(self): - #paint thread -- call redraw_canvas, which calls expose - self.redraw_canvas() - if (self._looping): - return True # keep running this event - else: - return False - - - def drawShape( self, ctx, poly, col ): - self.setColor( ctx, col ) - - for i in range ( 0, len(poly._xs) ): - ctx.line_to ( poly._xs[i], poly._ys[i] ) - ctx.close_path() - ctx.set_line_width(1) - ctx.stroke() - - - def fillShape( self, ctx, poly, col ): - self.setColor( ctx, col ) - for i in range ( 0, len(poly._xs) ): - ctx.line_to (poly._xs[i], poly._ys[i]) - ctx.close_path() - - ctx.fill() - - - def background( self, ctx, col, w, h ): - self.setColor( ctx, col ) - - ctx.line_to(0, 0) - ctx.line_to(w, 0) - ctx.line_to(w, h) - ctx.line_to(0, h) - ctx.close_path() - - ctx.fill() - - - def rect( self, ctx, x, y, w, h ): - ctx.line_to(x, y) - ctx.line_to(x+w, y) - ctx.line_to(x+w, y+h) - ctx.line_to(x, y+h) - ctx.close_path() - - - def setColor( self, ctx, col ): - if (not col._opaque): - ctx.set_source_rgba( col._r, col._g, col._b, col._a ) - else: - ctx.set_source_rgb( col._r, col._g, col._b ) - - - def line( self, ctx, x1, y1, x2, y2 ): - ctx.move_to (x1, y1) - ctx.line_to (x2, y2) - ctx.stroke() - - - def point( self, ctx, x1, y1 ): - self.line( ctx, x1, y1, x1+1, y1 )
\ No newline at end of file diff --git a/p5_button.py b/p5_button.py deleted file mode 100644 index cf76a34..0000000 --- a/p5_button.py +++ /dev/null @@ -1,173 +0,0 @@ -#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 p5 import P5 - -class P5Button(P5): - - def __init__(self): - P5.__init__(self) - self.noloop() - self._butts = [] - self._buttonPressed = False - - - def button_press(self, widget, event): - P5.button_press(self, widget, event) - - #iterate through the buttons to see if you've pressed any down - bp = False - for i in range ( 0, len(self._butts) ): - if (self._butts[i]._enabled): - contains = self._butts[i].contains(event.x, event.y) - self._butts[i]._pressed = contains - if (contains): - bp = True - - self._buttonPressed = bp - self.redraw() - - - def button_release(self, widget, event): - P5.button_release(self, widget, event) - self._buttonPressed = False - - pressed = [] - #iterate through the buttons to see if you've released on any - for i in range ( 0, len(self._butts) ): - if (self._butts[i]._enabled): - if (self._butts[i]._pressed): - if (self._butts[i].contains(event.x, event.y)): - pressed.append( self._butts[i] ) - - if (self._butts[i]._toggle): - self._butts[i]._pressed = not self._butts[i]._pressed - else: - self._butts[i]._pressed = False - - for i in range( 0, len(pressed) ): - pressed[i].doPressed() - - self.redraw() - - -class Polygon: - - def __init__( self, xs, ys ): - self.setPoints( xs, ys ) - - - def setPoints( self, xs, ys ): - self._xs = xs - self._ys = ys - - self._boundingX = self._xs[0] - self._boundingY = self._ys[0] - self._boundingW = self._xs[0] - self._boundingH = self._ys[0] - - for i in range ( 1, len(self._xs) ): - if (self._xs[i] > self._boundingW): - self._boundingW = self._xs[i] - if (self._ys[i] > self._boundingH): - self._boundingH = self._ys[i] - if (self._xs[i] < self._boundingX): - self._boundingX = self._xs[i] - if (self._ys[i] < self._boundingY): - self._boundingY = self._ys[i] - - - def contains( self, mx, my ): - if (not self.bbox_contains(mx, my)): - return False - - #insert simple path tracing check on the polygon here - - return True - - - def bbox_contains( self, mx, my ): - if ( not((mx>=self._boundingX) and (my>=self._boundingY) and (mx<self._boundingW) and (my<self._boundingH)) ): - return False - else: - return True - - -class Button: - - def __init__(self, poly, offX, offY): - self._poly = poly - self._offX = offX - self._offY = offY - - self._enabled = True - self._pressed = False - self._toggle = False - - self._listeners = [] - - self._actionCommand = None - - self._img = None - - - def setOffsets(self, offs): - self._offX = offs[0] - self._offY = offs[1] - - - def addActionListener(self, listen): - self._listeners.append(listen) - - - def removeActionListener(self, listen): - try: - self._listeners.remove(listen) - except ValueError: - pass - - - def setActionCommand(self, command): - self._actionCommand = command - - - def getActionCommand(self): - return self._actionCommand - - - def setImage(self, img): - self._img = img - - - def contains( self, mx, my ): - x = mx - self._offX - y = my - self._offY - - contains = self._poly.contains( x, y ) - return contains - - - def doPressed( self ): - for i in range ( 0, len(self._listeners) ): - self._listeners[i].fireButton( self._actionCommand ) - - - def isImg( self ): - return self._img != None
\ No newline at end of file diff --git a/port/AUTHORS b/port/AUTHORS deleted file mode 100644 index 47ead6c..0000000 --- a/port/AUTHORS +++ /dev/null @@ -1 +0,0 @@ -Aleksey Lim <alsroot@member.fsf.org> diff --git a/port/COPYING b/port/COPYING deleted file mode 100644 index 623b625..0000000 --- a/port/COPYING +++ /dev/null @@ -1,340 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc. - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Library General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - <one line to give the program's name and a brief idea of what it does.> - Copyright (C) <year> <name of author> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - <signature of Ty Coon>, 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Library General -Public License instead of this License. diff --git a/port/NEWS b/port/NEWS deleted file mode 100644 index 0dcfeb5..0000000 --- a/port/NEWS +++ /dev/null @@ -1,8 +0,0 @@ -1 - -* Add tarball.py -* Add json import wrapper -* Add object chooser -* Add activity classes -* Add pixbuf methods -* Add TempoSlider and ScrolledBox widgets diff --git a/port/README b/port/README deleted file mode 100644 index bd0dade..0000000 --- a/port/README +++ /dev/null @@ -1,13 +0,0 @@ -About ------ - -A set of sugar components/libraries/etc to simplify writing activities. - -Cornerstone purposes for this project: -* Total backwards compatibility for sugar-port API -* Run on all sugar platforms beginning from 0.82 - -Get it ------- - -http://wiki.sugarlabs.org/go/Development_Team/sugar-port diff --git a/port/TODO b/port/TODO deleted file mode 100644 index e69de29..0000000 --- a/port/TODO +++ /dev/null diff --git a/port/__init__.py b/port/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/port/__init__.py +++ /dev/null diff --git a/port/json.py b/port/json.py deleted file mode 100644 index d464abb..0000000 --- a/port/json.py +++ /dev/null @@ -1,33 +0,0 @@ -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - -""" -Unify usage of simplejson in Python 2.5/2.6 - -In Python 2.5 it imports simplejson module, in 2.6 native json module. - -Usage: - - import port.json as json - - # and using regular simplejson interface with module json - json.dumps([]) - -""" - -try: - from json import * - dumps -except (ImportError, NameError): - from simplejson import * @@ -18,43 +18,37 @@ #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN #THE SOFTWARE. -import gtk -import gobject import os -import shutil -import telepathy -import telepathy.client import logging -import xml.dom.minidom -import time -from xml.dom.minidom import parse +import shutil +from gettext import gettext as _ +from gettext import ngettext + +import gtk +from gtk import gdk +import cairo +import pangocairo import pygst pygst.require('0.10') import gst -import logging -logger = logging.getLogger('record:record.py') - +import sugar.profile from sugar.activity import activity -from sugar.presence import presenceservice -from sugar.presence.tubeconn import TubeConnection -from sugar import util -import port.json +from sugar.graphics.icon import Icon from model import Model -from ui import UI -from recordtube import RecordTube -from glive import Glive -from glivex import GliveX -from gplay import Gplay -from greplay import Greplay -from recorded import Recorded -from constants import Constants -import instance +from button import RecdButton +import constants from instance import Instance -import serialize import utils +from tray import HTray +from toolbarcombobox import ToolComboBox +from mediaview import MediaView +import hw +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) @@ -63,493 +57,886 @@ if logging.getLogger().level <= logging.DEBUG: else: gst.debug_set_default_threshold(gst.LEVEL_ERROR) - class Record(activity.Activity): - - log = logging.getLogger('record-activity') - def __init__(self, handle): - activity.Activity.__init__(self, handle) - #flags for controlling the writing to the datastore - self.I_AM_CLOSING = False - self.I_AM_SAVED = False - + super(Record, self).__init__(handle) self.props.enable_fullscreen_mode = False Instance(self) - Constants(self) - self.modify_bg( gtk.STATE_NORMAL, Constants.colorBlack.gColor ) - - #wait a moment so that our debug console capture mistakes - gobject.idle_add( self._initme, None ) - - - def _initme( self, userdata=None ): - #totally tubular - self.meshTimeoutTime = 10000 - self.recTube = None - self.connect( "shared", self._sharedCb ) #the main classes - self.m = Model(self) - self.glive = Glive(self) - self.glivex = GliveX(self) - self.gplay = Gplay(self) - self.ui = UI(self) + self.model = Model(self) + self.ui_init() #CSCL - if self._shared_activity: + self.connect("shared", self._shared_cb) + if self.get_shared_activity(): #have you joined or shared this activity yourself? if self.get_shared(): - self._meshJoinedCb( self ) + self._joined_cb(self) else: - self.connect("joined", self._meshJoinedCb) + self.connect("joined", self._joined_cb) - return False + # 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 + self._toolbox.set_current_toolbar(1) - def read_file(self, file): - try: - dom = parse(file) - except Exception, e: - logger.error('read_file: %s' % e) - return + def read_file(self, path): + self.model.read_file(path) - serialize.fillMediaHash(dom, self.m.mediaHashs) + def write_file(self, path): + self.model.write_file(path) - for i in dom.documentElement.getElementsByTagName('ui'): - for ui_el in i.childNodes: - self.ui.deserialize(port.json.loads(ui_el.data)) + 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 write_file(self, file): - self.I_AM_SAVED = False + def _joined_cb(self, activity): + self.model.collab.joined() - self.m.mediaHashs['ui'] = self.ui.serialize() + def ui_init(self): + self._fullscreen = False + self._showing_info = False - dom = serialize.saveMediaHash(self.m.mediaHashs) + # FIXME: if _thumb_tray becomes some kind of button group, we wouldn't + # have to track which recd is active + self._active_recd = None - ui_data = port.json.dumps(self.ui.serialize()) - ui_el = dom.createElement('ui') - ui_el.appendChild(dom.createTextNode(ui_data)) - dom.documentElement.appendChild(ui_el) + self.connect('key-press-event', self._key_pressed) - xmlFile = open( file, "w" ) - dom.writexml(xmlFile) - xmlFile.close() + self._active_toolbar_idx = 0 + self._toolbox = activity.ActivityToolbox(self) + self.set_toolbox(self._toolbox) + self._toolbar_modes = [None] - allDone = True - for h in range (0, len(self.m.mediaHashs)-1): - mhash = self.m.mediaHashs[h] - for i in range (0, len(mhash)): - recd = mhash[i] + # remove Toolbox's hardcoded grey separator + self._toolbox.remove(self._toolbox._separator) - if ( (not recd.savedMedia) or (not recd.savedXml) ): - allDone = False + if self.model.get_has_camera(): + self._photo_toolbar = PhotoToolbar() + self._toolbox.add_toolbar(_('Photo'), self._photo_toolbar) + self._toolbar_modes.append(constants.MODE_PHOTO) - if (self.I_AM_CLOSING): - mediaObject = recd.datastoreOb - if (mediaObject != None): - recd.datastoreOb = None - mediaObject.destroy() - del mediaObject + self._video_toolbar = VideoToolbar() + self._toolbox.add_toolbar(_('Video'), self._video_toolbar) + self._toolbar_modes.append(constants.MODE_VIDEO) + else: + self._photo_toolbar = None + self._video_toolbar = None - self.I_AM_SAVED = True - if (self.I_AM_SAVED and self.I_AM_CLOSING): - self.destroy() + self._audio_toolbar = AudioToolbar() + self._toolbox.add_toolbar(_('Audio'), self._audio_toolbar) + self._toolbar_modes.append(constants.MODE_AUDIO) + self._toolbox.show_all() + self._toolbox.connect("current-toolbar-changed", self._toolbar_changed) - def stopPipes(self): - self.ui.doMouseListener( False ) - self.m.setUpdating( False ) + main_box = gtk.VBox() + self.set_canvas(main_box) + main_box.get_parent().modify_bg(gtk.STATE_NORMAL, COLOR_BLACK) + main_box.show() - if (self.ui.COUNTINGDOWN): - self.m.abandonRecording() - elif (self.m.RECORDING): - self.m.doShutter() - else: - self.glive.stop() - self.glivex.stop() + 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() - def restartPipes(self): - if (not self.ui.TRANSCODING): - self.ui.updateModeChange( ) - self.ui.doMouseListener( True ) + 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) - def close( self ): - self.I_AM_CLOSING = True + self._play_button = PlayButton() + self._play_button.connect('clicked', self._play_pause_clicked) + self._controls_hbox.pack_start(self._play_button, expand=False) - self.m.UPDATING = False - if (self.ui != None): - self.ui.updateButtonSensitivities( ) - self.ui.doMouseListener( False ) - self.ui.hideAllWindows() - if (self.gplay != None): - self.gplay.stop( ) - if (self.glive != None): - self.glive.stop( ) - if self.glivex != None: - self.glivex.stop() + self._playback_scale = PlaybackScale(self.model) + self._controls_hbox.pack_start(self._playback_scale, expand=True, fill=True) - #this calls write_file - activity.Activity.close( self ) + self._progress = ProgressInfo() + self._controls_hbox.pack_start(self._progress, expand=True, fill=True) + self._title_label = gtk.Label() + self._title_label.set_markup("<b><span foreground='white'>"+_('Title:')+'</span></b>') + self._controls_hbox.pack_start(self._title_label, expand=False) - def destroy( self ): - if self.I_AM_SAVED: - activity.Activity.destroy( self ) + 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() - def _sharedCb( self, activity ): - self._setup() + 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() - id = self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].OfferDBusTube( Constants.SERVICE, {}) + def serialize(self): + data = {} + if self._photo_toolbar: + data['photo_timer'] = self._photo_toolbar.get_timer_idx() - def _meshJoinedCb( self, activity ): - if not self._shared_activity: - return + if self._video_toolbar: + data['video_timer'] = self._video_toolbar.get_timer_idx() + data['video_duration'] = self._video_toolbar.get_duration_idx() + data['video_quality'] = self._video_toolbar.get_quality() - self._setup() + data['audio_timer'] = self._audio_toolbar.get_timer_idx() + data['audio_duration'] = self._audio_toolbar.get_duration_idx() - self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].ListTubes( reply_handler=self._list_tubes_reply_cb, error_handler=self._list_tubes_error_cb) + return data + def deserialize(self, data): + if self._photo_toolbar: + self._photo_toolbar.set_timer_idx(data.get('photo_timer', 0)) - def _list_tubes_reply_cb(self, tubes): - for tube_info in tubes: - self._newTubeCb(*tube_info) + if self._video_toolbar: + self._video_toolbar.set_timer_idx(data.get('video_timer', 0)) + self._video_toolbar.set_duration_idx(data.get('video_duration', 0)) + self._video_toolbar.set_quality(data.get('video_quality', 0)) + self._audio_toolbar.set_timer_idx(data.get('audio_timer', 0)) + self._audio_toolbar.set_duration_idx(data.get('audio_duration')) - def _list_tubes_error_cb(self, e): - self.__class__.log.error('ListTubes() failed: %s', e) + 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 _setup(self): - #sets up the tubes... - if self._shared_activity is None: - self.__class__.log.error('_setup: Failed to share or join activity') + def _play_pause_clicked(self, widget): + self.model.play_pause() + + # 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._video_toolbar.get_quality() + + def get_selected_timer(self): + mode = self.model.get_mode() + if mode == constants.MODE_PHOTO: + return self._photo_toolbar.get_timer() + elif mode == constants.MODE_VIDEO: + return self._video_toolbar.get_timer() + elif mode == constants.MODE_AUDIO: + return self._audio_toolbar.get_timer() + + def get_selected_duration(self): + mode = self.model.get_mode() + if mode == constants.MODE_VIDEO: + return self._video_toolbar.get_duration() + elif mode == constants.MODE_AUDIO: + return self._audio_toolbar.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 - pservice = presenceservice.get_instance() - try: - name, path = pservice.get_preferred_connection() - self.conn = telepathy.client.Connection(name, path) - except: - self.__class__.log.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._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: - self.__class__.log.debug('Found our room: it has handle#%d "%s"', handle, self.conn.InspectHandles(htype, [handle])[0]) - room = handle - ctype = channel.GetChannelType() - if ctype == telepathy.CHANNEL_TYPE_TUBES: - self.__class__.log.debug('Found our Tubes channel at %s', channel_path) - tubes_chan = channel - elif ctype == telepathy.CHANNEL_TYPE_TEXT: - self.__class__.log.debug('Found our Text channel at %s', channel_path) - text_chan = channel - - if room is None: - self.__class__.log.error("Presence service didn't create a room") + self._shutter_button.hide() + self._countdown_image.set_value(value) + self._countdown_image.show() + + 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 text_chan is None: - self.__class__.log.error("Presence service didn't create a text channel") - return - - # Make sure we have a Tubes channel - PS doesn't yet provide one - if tubes_chan is None: - self.__class__.log.debug("Didn't find our Tubes channel, requesting one...") - tubes_chan = self.conn.request_channel(telepathy.CHANNEL_TYPE_TUBES, telepathy.HANDLE_TYPE_ROOM, room, True) - - self.tubes_chan = tubes_chan - self.text_chan = text_chan - - tubes_chan[telepathy.CHANNEL_TYPE_TUBES].connect_to_signal('NewTube', self._newTubeCb) - - - def _newTubeCb(self, id, initiator, type, service, params, state): - self.__class__.log.debug('New tube: ID=%d initator=%d type=%d service=%s params=%r state=%d', id, initiator, type, service, params, state) - if (type == telepathy.TUBE_TYPE_DBUS and service == Constants.SERVICE): - if state == telepathy.TUBE_STATE_LOCAL_PENDING: - self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].AcceptDBusTube(id) - tube_conn = TubeConnection(self.conn, self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES], id, group_iface=self.text_chan[telepathy.CHANNEL_INTERFACE_GROUP]) - self.recTube = RecordTube(tube_conn) - self.recTube.connect("new-recd", self._newRecdCb) - self.recTube.connect("recd-request", self._recdRequestCb) - self.recTube.connect("recd-bits-arrived", self._recdBitsArrivedCb) - self.recTube.connect("recd-unavailable", self._recdUnavailableCb) - - - def _newRecdCb( self, objectThatSentTheSignal, recorder, xmlString ): - self.__class__.log.debug('_newRecdCb') - dom = None - try: - dom = xml.dom.minidom.parseString(xmlString) - except: - self.__class__.log.error('Unable to parse mesh xml') - if (dom == None): + + if self._showing_info: + self._show_recd(recd, play=False) return - recd = Recorded() - recd = serialize.fillRecdFromNode(recd, dom.documentElement) - if (recd != None): - self.__class__.log.debug('_newRecdCb: adding new recd thumb') - recd.buddy = True - recd.downloadedFromBuddy = False - self.m.addMeshRecd( recd ) + self._showing_info = True + if self.model.get_mode() in (constants.MODE_PHOTO, constants.MODE_AUDIO): + func = self._media_view.show_info_photo else: - self.__class__.log.debug('_newRecdCb: recd is None. Unable to parse XML') + 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() - def requestMeshDownload( self, recd ): - if (recd.meshDownloading): - return True + func(recd.recorderName, recd.colorStroke, recd.colorFill, utils.getDateString(recd.time), recd.tags) - self.m.updateXoFullStatus() - if (self.m.FULL): - return True + def _media_view_full_clicked(self, widget): + self._toggle_fullscreen() - #this call will get the bits or request the bits if they're not available - if (recd.buddy and (not recd.downloadedFromBuddy)): - self.meshInitRoundRobin(recd) - return True + 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._toolbox.hide() + self._thumb_tray.hide() else: - return False + self._toolbox.show() + self._thumb_tray.show() + self._fullscreen = not self._fullscreen + self._media_view.set_fullscreen(self._fullscreen) - def meshInitRoundRobin( self, recd ): - if (recd.meshDownloading): - self.__class__.log.debug("meshInitRoundRobin: we are in midst of downloading this file...") + def _toolbar_changed(self, toolbox, num): + if num == 0: # Activity tab return - if (self.recTube == None): - gobject.idle_add(self.ui.updateMeshProgress, False, recd) + # Prevent mode/tab changing under certain conditions by immediately + # changing back to the previously-selected toolbar + if self.model.ui_frozen(): + self._toolbox.set_current_toolbar(self._active_toolbar_idx) return - #start with who took the photo - recd.triedMeshBuddies = [] - recd.triedMeshBuddies.append(Instance.keyHashPrintable) - self.meshReqRecFromBuddy( recd, recd.recorderHash, recd.recorderName ) - - - def meshNextRoundRobinBuddy( self, recd ): - self.__class__.log.debug('meshNextRoundRobinBuddy') - if (recd.meshReqCallbackId != 0): - gobject.source_remove(recd.meshReqCallbackId) - recd.meshReqCallbackId = 0 - - #delete any stub of a partially downloaded file - filepath = recd.getMediaFilepath() - if (filepath != None): - if (os.path.exists(filepath)): - os.remove( filepath ) - - goodBudObj = None - buds = self._shared_activity.get_joined_buddies() - for i in range (0, len(buds)): - nextBudObj = buds[i] - nextBud = util.sha_data(nextBudObj.props.key) - nextBud = util.printable_hash(nextBud) - if (recd.triedMeshBuddies.count(nextBud) > 0): - self.__class__.log.debug('mnrrb: weve already tried bud ' + str(nextBudObj.props.nick)) - else: - self.__class__.log.debug('mnrrb: ask next buddy: ' + str(nextBudObj.props.nick)) - goodBudObj = nextBudObj - break - - if (goodBudObj != None): - goodNick = goodBudObj.props.nick - goodBud = util.sha_data(goodBudObj.props.key) - goodBud = util.printable_hash(goodBud) - self.meshReqRecFromBuddy(recd, goodBud, goodNick) - else: - self.__class__.log.debug('weve tried all buddies here, and no one has this recd') - recd.meshDownloading = False - recd.triedMeshBuddies = [] - recd.triedMeshBuddies.append(Instance.keyHashPrintable) - self.ui.updateMeshProgress(False, recd) - - - def meshReqRecFromBuddy( self, recd, fromWho, fromWhosNick ): - recd.triedMeshBuddies.append( fromWho ) - recd.meshDownloadingFrom = fromWho - recd.meshDownloadingFromNick = fromWhosNick - recd.meshDownloadingProgress = False - recd.meshDownloading = True - recd.meshDownlodingPercent = 0.0 - self.ui.updateMeshProgress(True, recd) - recd.meshReqCallbackId = gobject.timeout_add(self.meshTimeoutTime, self._meshCheckOnRecdRequest, recd) - self.recTube.requestRecdBits( Instance.keyHashPrintable, fromWho, recd.mediaMd5 ) - - - def _meshCheckOnRecdRequest( self, recdRequesting ): - #todo: add category for "not active activity, so go ahead and delete" - - if (recdRequesting.downloadedFromBuddy): - self.__class__.log.debug('_meshCheckOnRecdRequest: recdRequesting.downloadedFromBuddy') - if (recdRequesting.meshReqCallbackId != 0): - gobject.source_remove(recdRequesting.meshReqCallbackId) - recdRequesting.meshReqCallbackId = 0 - return False - if (recdRequesting.deleted): - self.__class__.log.debug('_meshCheckOnRecdRequest: recdRequesting.deleted') - if (recdRequesting.meshReqCallbackId != 0): - gobject.source_remove(recdRequesting.meshReqCallbackId) - recdRequesting.meshReqCallbackId = 0 - return False - if (recdRequesting.meshDownloadingProgress): - self.__class__.log.debug('_meshCheckOnRecdRequest: recdRequesting.meshDownloadingProgress') - #we've received some bits since last we checked, so keep waiting... they'll all get here eventually! - recdRequesting.meshDownloadingProgress = False - return True + self._active_toolbar_idx = num + self.model.change_mode(self._toolbar_modes[num]) + + 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): + 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.__class__.log.debug('_meshCheckOnRecdRequest: ! recdRequesting.meshDownloadingProgress') - #that buddy we asked info from isn't responding; next buddy! - #self.meshNextRoundRobinBuddy( recdRequesting ) - gobject.idle_add(self.meshNextRoundRobinBuddy, recdRequesting) - return False + self._play_button.set_pause() + def _thumbnail_clicked(self, button, recd): + if self.model.ui_frozen(): + return - def _recdRequestCb( self, objectThatSentTheSignal, whoWantsIt, md5sumOfIt ): - #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.m.getRecdByMd5( md5sumOfIt ) - if (recd == None): - self.__class__.log.debug('_recdRequestCb: we dont have the recd they asked for') - self.recTube.unavailableRecd(md5sumOfIt, Instance.keyHashPrintable, whoWantsIt) + 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 (recd.deleted): - self.__class__.log.debug('_recdRequestCb: we have the recd, but it has been deleted, so we wont share') - self.recTube.unavailableRecd(md5sumOfIt, Instance.keyHashPrintable, whoWantsIt) + if not recd.isClipboardCopyable(): return - if (recd.buddy and not recd.downloadedFromBuddy): - self.__class__.log.debug('_recdRequestCb: we have an incomplete recd, so we wont share') - self.recTube.unavailableRecd(md5sumOfIt, Instance.keyHashPrintable, whoWantsIt) + + 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 - recd.meshUploading = True - filepath = recd.getMediaFilepath() + 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) - if (recd.type == Constants.TYPE_AUDIO): - audioImgFilepath = recd.getAudioImageFilepath() + 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(None, w, h, 24) + 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) - destPath = os.path.join(Instance.instancePath, "audioBundle") - destPath = utils.getUniqueFilepath(destPath, 0) - cmd = "cat " + str(filepath) + " " + str(audioImgFilepath) + " > " + str(destPath) - self.__class__.log.debug(cmd) - os.system(cmd) - filepath = destPath + def set_normal(self): + self.set_image(self._rec_image) - sent = self.recTube.broadcastRecd(recd.mediaMd5, filepath, whoWantsIt) - recd.meshUploading = False - #if you were deleted while uploading, now throw away those bits now - if (recd.deleted): - recd.doDeleteRecorded(recd) + def set_recording(self): + self.set_image(self._rec_red_image) - def _recdBitsArrivedCb( self, objectThatSentTheSignal, md5sumOfIt, part, numparts, bytes, fromWho ): - #self.__class__.log.debug('_recdBitsArrivedCb: ' + str(part) + "/" + str(numparts)) - recd = self.m.getRecdByMd5( md5sumOfIt ) - if (recd == None): - self.__class__.log.debug('_recdBitsArrivedCb: thx 4 yr bits, but we dont even have that photo') - return - if (recd.deleted): - self.__class__.log.debug('_recdBitsArrivedCb: thx 4 yr bits, but we deleted that photo') - return - if (recd.downloadedFromBuddy): - self.__class__.log.debug('_recdBitsArrivedCb: weve already downloadedFromBuddy') - return - if (not recd.buddy): - self.__class__.log.debug('_recdBitsArrivedCb: uh, we took this photo, so dont need your bits') - return - if (recd.meshDownloadingFrom != fromWho): - self.__class__.log.debug('_recdBitsArrivedCb: wrong bits ' + str(fromWho) + ", exp:" + str(recd.meshDownloadingFrom)) - return +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) - #update that we've heard back about this, reset the timeout - gobject.source_remove(recd.meshReqCallbackId) - recd.meshReqCallbackId = gobject.timeout_add(self.meshTimeoutTime, self._meshCheckOnRecdRequest, recd) - - #update the progress bar - recd.meshDownlodingPercent = (part+0.0)/(numparts+0.0) - recd.meshDownloadingProgress = True - self.ui.updateMeshProgress(True, recd) - f = open(recd.getMediaFilepath(), 'a+') - f.write(bytes) - f.close() - - if part == numparts: - self.__class__.log.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): - filepath = recd.getMediaFilepath() - bundlePath = os.path.join(Instance.instancePath, "audioBundle") - bundlePath = utils.getUniqueFilepath(bundlePath, 0) - - cmd = "split -a 1 -b " + str(recd.mediaBytes) + " " + str(filepath) + " " + str(bundlePath) - self.__class__.log.debug( cmd ) - os.system( cmd ) - - bundleName = os.path.basename(bundlePath) - mediaFilename = str(bundleName) + "a" - mediaFilepath = os.path.join(Instance.instancePath, mediaFilename) - mediaFilepathExt = os.path.join(Instance.instancePath, mediaFilename+".ogg") - os.rename(mediaFilepath, mediaFilepathExt) - audioImageFilename = str(bundleName) + "b" - audioImageFilepath = os.path.join(Instance.instancePath, audioImageFilename) - audioImageFilepathExt = os.path.join(Instance.instancePath, audioImageFilename+".png") - os.rename(audioImageFilepath, audioImageFilepathExt) - - recd.mediaFilename = os.path.basename(mediaFilepathExt) - recd.audioImageFilename = os.path.basename(audioImageFilepathExt) - - self.ui.showMeshRecd( recd ) - elif part > numparts: - self.__class__.log.error('More parts than required have arrived') - - - def _getAlbumArtCb( self, objectThatSentTheSignal, pixbuf, recd ): - - if (pixbuf != None): - imagePath = os.path.join(Instance.instancePath, "audioPicture.png") - imagePath = utils.getUniqueFilepath( imagePath, 0 ) - pixbuf.save( imagePath, "png", {} ) - recd.audioImageFilename = os.path.basename(imagePath) - - self.ui.showMeshRecd( recd ) - return False + 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) - def _recdUnavailableCb( self, objectThatSentTheSignal, md5sumOfIt, whoDoesntHaveIt ): - self.__class__.log.debug('_recdUnavailableCb: sux, we want to see that photo') - recd = self.m.getRecdByMd5( md5sumOfIt ) - if (recd == None): - self.__class__.log.debug('_recdUnavailableCb: actually, we dont even know about that one..') - return - if (recd.deleted): - self.__class__.log.debug('_recdUnavailableCb: actually, since we asked, we deleted.') - return - if (not recd.buddy): - self.__class__.log.debug('_recdUnavailableCb: uh, odd, we took that photo and have it already.') - return - if (recd.downloadedFromBuddy): - self.__class__.log.debug('_recdUnavailableCb: we already downloaded it... you might have been slow responding.') - return - if (recd.meshDownloadingFrom != whoDoesntHaveIt): - self.__class__.log.debug('_recdUnavailableCb: we arent asking you for a copy now. slow response, pbly.') - return + self.set_play() + + def set_play(self): + self.set_image(self._play_image) + + def set_pause(self): + self.set_image(self._pause_image) + + +class RecordToolbar(gtk.Toolbar): + def __init__(self, icon_name, with_quality, with_timer, with_duration): + super(RecordToolbar, self).__init__() + + img = Icon(icon_name=icon_name) + color = sugar.profile.get_color() + img.set_property('fill-color', color.get_fill_color()) + img.set_property('stroke-color', color.get_stroke_color()) + toolitem = gtk.ToolItem() + toolitem.add(img) + self.insert(toolitem, -1) + + separator = gtk.SeparatorToolItem() + separator.set_draw(False) + separator.set_expand(True) + self.insert(separator, -1) + + if with_quality: + 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.insert(self.quality, -1) + + if with_timer: + self._timer_combo = TimerCombo() + self.insert(self._timer_combo, -1) + + if with_duration: + self._duration_combo = DurationCombo() + self.insert(self._duration_combo, -1) + + 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 PhotoToolbar(RecordToolbar): + def __init__(self): + super(PhotoToolbar, self).__init__('media-photo', with_quality=False, with_timer=True, with_duration=False) + + +class VideoToolbar(RecordToolbar): + def __init__(self): + super(VideoToolbar, self).__init__('media-video', with_quality=True, with_timer=True, with_duration=True) + + +class AudioToolbar(RecordToolbar): + def __init__(self): + super(AudioToolbar, self).__init__('media-audio', with_quality=False, with_timer=True, with_duration=True) + + +class TimerCombo(ToolComboBox): + TIMERS = (0, 5, 10) + + def __init__(self): + self._combo_box_text = gtk.combo_box_new_text() + super(TimerCombo, self).__init__(combo=self._combo_box_text, label_text=_('Timer:')) + + for i in self.TIMERS: + if i == 0: + self._combo_box_text.append_text(_('Immediate')) + else: + string = TimerCombo._seconds_string(i) + self._combo_box_text.append_text(string) + self._combo_box_text.set_active(0) + + def get_value(self): + return TimerCombo.TIMERS[self._combo_box_text.get_active()] + + def get_value_idx(self): + return self._combo_box_text.get_active() + + def set_value_idx(self, idx): + self._combo_box_text.set_active(idx) + + @staticmethod + def _seconds_string(x): + return ngettext('%s second', '%s seconds', x) % x + + +class DurationCombo(ToolComboBox): + DURATIONS = (2, 4, 6) + + def __init__(self): + self._combo_box_text = gtk.combo_box_new_text() + super(DurationCombo, self).__init__(combo=self._combo_box_text, label_text=_('Duration:')) + + for i in self.DURATIONS: + string = DurationCombo._minutes_string(i) + self._combo_box_text.append_text(string) + self._combo_box_text.set_active(0) + + def get_value(self): + return 60 * self.DURATIONS[self._combo_box_text.get_active()] + + def get_value_idx(self): + return self._combo_box_text.get_active() + + def set_value_idx(self, idx): + self._combo_box_text.set_active(idx) + + @staticmethod + def _minutes_string(x): + return ngettext('%s minute', '%s minutes', x) % x - #self.meshNextRoundRobinBuddy( recd ) diff --git a/recorded.py b/recorded.py index 8639a70..27250d3 100644 --- a/recorded.py +++ b/recorded.py @@ -20,16 +20,13 @@ import os import gtk -from gtk import gdk -from constants import Constants +import constants from instance import Instance import utils import serialize -import record class Recorded: - def __init__( self ): self.type = -1 self.time = None @@ -78,6 +75,8 @@ class Recorded: def setTitle( self, newTitle ): + if self.title == newTitle: + return self.title = newTitle self.metaChange = True @@ -106,11 +105,11 @@ class Recorded: #relaunch, their old media -- datastoreObject->file (hold onto the datastore object, delete if deleted) | ([request->]) Journal/session/buddy def getThumbPixbuf( self ): - thumbPixbuf = None thumbFilepath = self.getThumbFilepath() - if ( os.path.isfile(thumbFilepath) ): - thumbPixbuf = gtk.gdk.pixbuf_new_from_file(thumbFilepath) - return thumbPixbuf + if os.path.isfile(thumbFilepath): + return gtk.gdk.pixbuf_new_from_file(thumbFilepath) + else: + return None def getThumbFilepath( self ): @@ -152,7 +151,7 @@ class Recorded: else: if self.mediaFilename == None: #creating a new filepath, probably just got here from the mesh - ext = Constants.mediaTypes[self.type][Constants.keyExt] + 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) @@ -170,4 +169,4 @@ class Recorded: print("RecordActivity error -- unable to get datastore object in getMediaFilepath") return None - return self.datastoreOb.file_path
\ No newline at end of file + return self.datastoreOb.file_path diff --git a/recordtube.py b/recordtube.py index 7f63922..c2a60bb 100644 --- a/recordtube.py +++ b/recordtube.py @@ -1,12 +1,14 @@ +import os +import logging + import gobject -from dbus import Interface -from dbus.service import method, signal +from dbus.service import signal from dbus.gobject_service import ExportedGObject -import os -from constants import Constants +import constants from instance import Instance -import record + +logger = logging.getLogger('recordtube') class RecordTube(ExportedGObject): @@ -23,46 +25,46 @@ class RecordTube(ExportedGObject): def __init__(self, tube): - super(RecordTube, self).__init__(tube, Constants.PATH) + super(RecordTube, self).__init__(tube, constants.DBUS_PATH) self.tube = tube - self.idNotify = self.tube.add_signal_receiver(self._newRecdTubeCb, 'notifyBudsOfNewRecd', Constants.IFACE, path=Constants.PATH, sender_keyword='sender') - self.idRequest = self.tube.add_signal_receiver(self._reqRecdTubeCb, 'requestRecdBits', Constants.IFACE, path=Constants.PATH, sender_keyword='sender') - self.idBroadcast = self.tube.add_signal_receiver(self._getRecdTubeCb, 'broadcastRecdBits', Constants.IFACE, path=Constants.PATH, sender_keyword='sender', byte_arrays=True) - self.idUnavailable = self.tube.add_signal_receiver(self._unavailableRecdTubeCb, 'unavailableRecd', Constants.IFACE, path=Constants.PATH, sender_keyword='sender') + self.idNotify = self.tube.add_signal_receiver(self._newRecdTubeCb, 'notifyBudsOfNewRecd', constants.DBUS_IFACE, path=constants.DBUS_PATH, sender_keyword='sender') + self.idRequest = self.tube.add_signal_receiver(self._reqRecdTubeCb, 'requestRecdBits', constants.DBUS_IFACE, path=constants.DBUS_PATH, sender_keyword='sender') + self.idBroadcast = self.tube.add_signal_receiver(self._getRecdTubeCb, 'broadcastRecdBits', constants.DBUS_IFACE, path=constants.DBUS_PATH, sender_keyword='sender', byte_arrays=True) + self.idUnavailable = self.tube.add_signal_receiver(self._unavailableRecdTubeCb, 'unavailableRecd', constants.DBUS_IFACE, path=constants.DBUS_PATH, sender_keyword='sender') - @signal(dbus_interface=Constants.IFACE, signature='ss') #dual s for 2x strings + @signal(dbus_interface=constants.DBUS_IFACE, signature='ss') #dual s for 2x strings def notifyBudsOfNewRecd(self, recorder, recdXml): - record.Record.log.debug('Ive taken a new pho-ideo-audio! I hereby send you an xml thumb of said media via this interface.') + logger.debug('Ive taken a new pho-ideo-audio! I hereby send you an xml thumb of said media via this interface.') def _newRecdTubeCb(self, recorder, recdXml, sender=None): - record.Record.log.debug("_newRecdTubeCb from " + recorder ) + logger.debug("_newRecdTubeCb from " + recorder ) if sender == self.tube.get_unique_name(): - record.Record.log.debug("_newRecdTubeCb: sender is my bus name, so ignore my own signal") + logger.debug("_newRecdTubeCb: sender is my bus name, so ignore my own signal") return elif (recorder == Instance.keyHashPrintable): - record.Record.log.debug('_newRecdTubeCb: excuse me? you are asking me to share a photo with myself?') + logger.debug('_newRecdTubeCb: excuse me? you are asking me to share a photo with myself?') return self.emit( "new-recd", str(recorder), str(recdXml) ) - @signal(dbus_interface=Constants.IFACE, signature='sss') #triple s for 3x strings + @signal(dbus_interface=constants.DBUS_IFACE, signature='sss') #triple s for 3x strings def requestRecdBits(self, whoWantsIt, whoTheyWantItFrom, recdMd5sumOfIt ): - record.Record.log.debug('I am requesting a high-res version of someones media.') + logger.debug('I am requesting a high-res version of someones media.') def _reqRecdTubeCb(self, whoWantsIt, whoTheyWantItFrom, recdMd5sumOfIt, sender=None): if sender == self.tube.get_unique_name(): - record.Record.log.debug("_reqRecdTubeCb: sender is my bus name, so ignore my own signal") + logger.debug("_reqRecdTubeCb: sender is my bus name, so ignore my own signal") return elif (whoWantsIt == Instance.keyHashPrintable): - record.Record.log.debug('_reqRecdTubeCb: excuse me? you are asking me to share a photo with myself?') + logger.debug('_reqRecdTubeCb: excuse me? you are asking me to share a photo with myself?') return elif (whoTheyWantItFrom != Instance.keyHashPrintable): - record.Record.log.debug('_reqRecdTubeCb: ive overhead someone wants a photo, but not from me') + logger.debug('_reqRecdTubeCb: ive overhead someone wants a photo, but not from me') return self.emit( "recd-request", str(whoWantsIt), str(recdMd5sumOfIt) ) @@ -78,17 +80,17 @@ class RecordTube(ExportedGObject): for chunk in range(chunks): bytes = f.read(chunk_size) - if (chunk == 0): - record.Record.log.debug("sending " + str(chunk+1) + " of " + str(chunks) + " to " + sendThisTo ) - if (chunk == chunks-1): - record.Record.log.debug("sending " + str(chunk+1) + " of " + str(chunks) + " to " + sendThisTo ) + if chunk == 0: + logger.debug("sending " + str(chunk+1) + " of " + str(chunks) + " to " + sendThisTo ) + if chunk == chunks-1: + logger.debug("sending " + str(chunk+1) + " of " + str(chunks) + " to " + sendThisTo ) self.broadcastRecdBits(md5, chunk+1, chunks, bytes, sendThisTo, Instance.keyHashPrintable) f.close() return True - @signal(dbus_interface=Constants.IFACE, signature='suuayss') + @signal(dbus_interface=constants.DBUS_IFACE, signature='suuayss') def broadcastRecdBits(self, md5, part, numparts, bytes, sendTo, fromWho ): pass @@ -107,20 +109,20 @@ class RecordTube(ExportedGObject): self.emit( "recd-bits-arrived", md5, part, numparts, bytes, fromWho ) - @signal(dbus_interface=Constants.IFACE, signature='sss') #triple s for 3x strings + @signal(dbus_interface=constants.DBUS_IFACE, signature='sss') #triple s for 3x strings def unavailableRecd(self, md5sumOfIt, whoDoesntHaveIt, whoAskedForIt): - record.Record.log.debug('unavailableRecd: id love to share this photo, but i am without a copy meself chum') + logger.debug('unavailableRecd: id love to share this photo, but i am without a copy meself chum') def _unavailableRecdTubeCb( self, md5sumOfIt, whoDoesntHaveIt, whoAskedForIt, sender=None): if sender == self.tube.get_unique_name(): - record.Record.log.debug("_unavailableRecdTubeCb: sender is my bus name, so ignore my own signal") + logger.debug("_unavailableRecdTubeCb: sender is my bus name, so ignore my own signal") return - if (whoDoesntHaveIt == Instance.keyHashPrintable): - record.Record.log.debug('_unavailableRecdTubeCb: yes, i know i dont have it, i just told you/me/us.') + if whoDoesntHaveIt == Instance.keyHashPrintable: + logger.debug('_unavailableRecdTubeCb: yes, i know i dont have it, i just told you/me/us.') return - if (whoAskedForIt != Instance.keyHashPrintable): - record.Record.log.debug('_unavailableRecdTubeCb: ive overheard someone doesnt have a photo, but i didnt ask for that one anyways') + if whoAskedForIt != Instance.keyHashPrintable: + logger.debug('_unavailableRecdTubeCb: ive overheard someone doesnt have a photo, but i didnt ask for that one anyways') return - self.emit("recd-unavailable", md5sumOfIt, whoDoesntHaveIt)
\ No newline at end of file + self.emit("recd-unavailable", md5sumOfIt, whoDoesntHaveIt) diff --git a/serialize.py b/serialize.py index 8e56d5f..82618fc 100644 --- a/serialize.py +++ b/serialize.py @@ -1,162 +1,134 @@ -import xml.dom.minidom from xml.dom.minidom import getDOMImplementation -from xml.dom.minidom import parse import cStringIO import os import gtk +import logging from sugar.datastore import datastore -from constants import Constants +import constants from instance import Instance -from color import Color -import record -import utils import recorded +import utils -def fillMediaHash( doc, mediaHashs ): - for key,value in Constants.mediaTypes.items(): - recdElements = doc.documentElement.getElementsByTagName(value[Constants.keyName]) +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 ): +def _loadMediaIntoHash(el, hash): addToHash = True - recd = record.Recorded() + recd = recorded.Recorded() recd = fillRecdFromNode(recd, el) - if (recd != None): - if (recd.datastoreId != None): + 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 (recd.datastoreOb == None): + 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']): + if recd.title != recd.datastoreOb.metadata['title']: recd.setTitle(recd.datastoreOb.metadata['title']) - if (recd.tags != recd.datastoreOb.metadata['tags']): + if recd.tags != recd.datastoreOb.metadata['tags']: recd.setTags(recd.datastoreOb.metadata['tags']) - if (recd.buddy): + if recd.buddy: recd.downloadedFromBuddy = True recd.datastoreOb == None - if (addToHash): - hash.append( recd ) + if addToHash: + hash.append(recd ) - -def getMediaFromDatastore( recd ): - if (recd.datastoreId == None): +def getMediaFromDatastore(recd): + if not recd.datastoreId: return None - if (recd.datastoreOb != None): + if recd.datastoreOb: #already have the object return recd.datastoreOb mediaObject = None try: - mediaObject = datastore.get( recd.datastoreId ) + mediaObject = datastore.get(recd.datastoreId) finally: - if (mediaObject == None): - return None - - return mediaObject + return mediaObject - -def removeMediaFromDatastore( recd ): +def removeMediaFromDatastore(recd): #before this method is called, the media are removed from the file - if (recd.datastoreId == None): - return - if (recd.datastoreOb == None): + if not recd.datastoreId or not recd.datastoreOb: return try: recd.datastoreOb.destroy() - datastore.delete( recd.datastoreId ) + datastore.delete(recd.datastoreId) - del recd.datastoreId recd.datastoreId = None - - del recd.datastoreOb recd.datastoreOb = None - finally: #todo: add error message here pass +def fillRecdFromNode(recd, el): + if el.getAttributeNode('type'): + recd.type = int(el.getAttribute('type')) -def fillRecdFromNode( recd, el ): - if (el.getAttributeNode(Constants.recdType) != None): - typeInt = int(el.getAttribute(Constants.recdType)) - recd.type = typeInt + if el.getAttributeNode('title'): + recd.title = el.getAttribute('title') - if (el.getAttributeNode(Constants.recdTitle) != None): - recd.title = el.getAttribute(Constants.recdTitle) + if el.getAttributeNode('time'): + recd.time = int(el.getAttribute('time')) - if (el.getAttributeNode(Constants.recdTime) != None): - timeInt = int(el.getAttribute(Constants.recdTime)) - recd.time = timeInt + if el.getAttributeNode('photographer'): + recd.recorderName = el.getAttribute('photographer') - if (el.getAttributeNode(Constants.recdRecorderName) != None): - recd.recorderName = el.getAttribute(Constants.recdRecorderName) - - if (el.getAttributeNode(Constants.recdTags) != None): - recd.tags = el.getAttribute(Constants.recdTags) + if el.getAttributeNode('tags'): + recd.tags = el.getAttribute('tags') else: recd.tags = "" - if (el.getAttributeNode(Constants.recdRecorderHash) != None): - recd.recorderHash = el.getAttribute(Constants.recdRecorderHash) + if el.getAttributeNode('recorderHash'): + recd.recorderHash = el.getAttribute('recorderHash') - if (el.getAttributeNode(Constants.recdColorStroke) != None): - try: - colorStrokeHex = el.getAttribute(Constants.recdColorStroke) - colorStroke = Color() - colorStroke.init_hex(colorStrokeHex) - recd.colorStroke = colorStroke - except: - record.Record.log.error("unable to load recd colorStroke") + if el.getAttributeNode('colorStroke'): + recd.colorStroke = el.getAttribute('colorStroke') - if (el.getAttributeNode(Constants.recdColorFill) != None): - try: - colorFillHex = el.getAttribute(Constants.recdColorFill) - colorFill = Color() - colorFill.init_hex( colorFillHex ) - recd.colorFill = colorFill - except: - record.Record.log.error("unable to load recd colorFill") + if el.getAttributeNode('colorFill'): + recd.colorFill = el.getAttribute('colorFill') - if (el.getAttributeNode(Constants.recdBuddy) != None): - recd.buddy = (el.getAttribute(Constants.recdBuddy) == "True") + if el.getAttributeNode('buddy'): + recd.buddy = (el.getAttribute('buddy') == "True") - if (el.getAttributeNode(Constants.recdMediaMd5) != None): - recd.mediaMd5 = el.getAttribute(Constants.recdMediaMd5) + if el.getAttributeNode('mediaMd5'): + recd.mediaMd5 = el.getAttribute('mediaMd5') - if (el.getAttributeNode(Constants.recdThumbMd5) != None): - recd.thumbMd5 = el.getAttribute(Constants.recdThumbMd5) + if el.getAttributeNode('thumbMd5'): + recd.thumbMd5 = el.getAttribute('thumbMd5') - if (el.getAttributeNode(Constants.recdMediaBytes) != None): - recd.mediaBytes = el.getAttribute(Constants.recdMediaBytes) + if el.getAttributeNode('mediaBytes'): + recd.mediaBytes = el.getAttribute('mediaBytes') - if (el.getAttributeNode(Constants.recdThumbBytes) != None): - recd.thumbBytes = el.getAttribute(Constants.recdThumbBytes) + if el.getAttributeNode('thumbBytes'): + recd.thumbBytes = el.getAttribute('thumbBytes') - bt = el.getAttributeNode(Constants.recdBase64Thumb) - if (bt != None): + 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 ) + thumbPath = utils.getUniqueFilepath(thumbPath, 0) + thumbImg = utils.getPixbufFromString(bt.nodeValue) thumbImg.save(thumbPath, "jpeg", {"quality":"85"} ) recd.thumbFilename = os.path.basename(thumbPath) - record.Record.log.debug("saved thumbFilename") + logger.debug("saved thumbFilename") except: - record.Record.log.error("unable to getRecdBase64Thumb") + logger.error("unable to getRecdBase64Thumb") - ai = el.getAttributeNode(Constants.recdAudioImage) + ai = el.getAttributeNode('audioImage') if (not ai == None): try: audioImagePath = os.path.join(Instance.instancePath, "audioImage.png") @@ -164,129 +136,120 @@ def fillRecdFromNode( recd, el ): audioImage = utils.getPixbufFromString( ai.nodeValue ) audioImage.save(audioImagePath, "png", {} ) recd.audioImageFilename = os.path.basename(audioImagePath) - record.Record.log.debug("loaded audio image and set audioImageFilename") + logger.debug("loaded audio image and set audioImageFilename") except: - record.Record.log.error("unable to load audio image") + logger.error("unable to load audio image") - datastoreNode = el.getAttributeNode(Constants.recdDatastoreId) - if (datastoreNode != None): + datastoreNode = el.getAttributeNode('datastoreId') + if datastoreNode: recd.datastoreId = datastoreNode.nodeValue return recd -def getRecdXmlMeshString( recd ): +def getRecdXmlMeshString(recd): impl = getDOMImplementation() - recdXml = impl.createDocument(None, Constants.recdRecd, None) + recdXml = impl.createDocument(None, 'recd', None) root = recdXml.documentElement - _addRecdXmlAttrs( root, recd, True ) + _addRecdXmlAttrs(root, recd, True) writer = cStringIO.StringIO() recdXml.writexml(writer) return writer.getvalue() - -def _addRecdXmlAttrs( el, recd, forMeshTransmit ): - el.setAttribute(Constants.recdType, str(recd.type)) - - if ((recd.type == Constants.TYPE_AUDIO) and (not forMeshTransmit)): - aiPixbuf = recd.getAudioImagePixbuf( ) - aiPixbufString = str( utils.getStringFromPixbuf(aiPixbuf) ) - el.setAttribute(Constants.recdAudioImage, aiPixbufString) - - if ((recd.datastoreId != None) and (not forMeshTransmit)): - el.setAttribute(Constants.recdDatastoreId, str(recd.datastoreId)) - - el.setAttribute(Constants.recdTitle, recd.title) - el.setAttribute(Constants.recdTime, str(recd.time)) - el.setAttribute(Constants.recdRecorderName, recd.recorderName) - el.setAttribute(Constants.recdRecorderHash, str(recd.recorderHash) ) - el.setAttribute(Constants.recdColorStroke, str(recd.colorStroke.hex) ) - el.setAttribute(Constants.recdColorFill, str(recd.colorFill.hex) ) - el.setAttribute(Constants.recdBuddy, str(recd.buddy)) - el.setAttribute(Constants.recdMediaMd5, str(recd.mediaMd5)) - el.setAttribute(Constants.recdThumbMd5, str(recd.thumbMd5)) - el.setAttribute(Constants.recdMediaBytes, str(recd.mediaBytes)) - el.setAttribute(Constants.recdThumbBytes, str(recd.thumbBytes)) - el.setAttribute(Constants.recdRecordVersion, str(Constants.VERSION)) - - pixbuf = recd.getThumbPixbuf( ) - thumb64 = str( utils.getStringFromPixbuf(pixbuf) ) - el.setAttribute(Constants.recdBase64Thumb, thumb64) - - -def saveMediaHash( mediaHashs ): +def _addRecdXmlAttrs(el, recd, forMeshTransmit): + el.setAttribute('type', str(recd.type)) + + if (recd.type == constants.TYPE_AUDIO) and (not forMeshTransmit): + aiPixbuf = recd.getAudioImagePixbuf() + 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)) + 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() + thumb64 = str(utils.getStringFromPixbuf(pixbuf)) + el.setAttribute('base64Thumb', thumb64) + +def saveMediaHash(mediaHashs, activity): impl = getDOMImplementation() - album = impl.createDocument(None, Constants.recdAlbum, None) + album = impl.createDocument(None, 'album', None) root = album.documentElement #flag everything for saving... atLeastOne = False - for type,value in Constants.mediaTypes.items(): - typeName = value[Constants.keyName] - hash = mediaHashs[type] - for i in range (0, len(hash)): - recd = hash[i] + 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.mediaTypes.items(): - typeName = value[Constants.keyName] - hash = mediaHashs[type] - - for i in range (0, len(hash)): - recd = hash[i] - mediaEl = album.createElement( typeName ) - root.appendChild( mediaEl ) - _saveMedia( mediaEl, recd ) + 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 ): - if ( (recd.buddy == True) and (recd.datastoreId == None) and (not recd.downloadedFromBuddy) ): +def _saveMedia(el, recd, activity): + if recd.buddy == True and recd.datastoreId == None and not recd.downloadedFromBuddy: recd.savedMedia = True - _saveXml( el, recd ) + _saveXml(el, recd) else: recd.savedMedia = False - _saveMediaToDatastore( el, recd ) + _saveMediaToDatastore(el, recd, activity) - -def _saveXml( el, recd ): - if (recd.thumbFilename != None): - _addRecdXmlAttrs( el, recd, False ) +def _saveXml(el, recd): + if recd.thumbFilename: + _addRecdXmlAttrs(el, recd, False) else: - record.Record.log.debug("WOAH, ERROR: recd has no thumbFilename?! " + str(recd) ) + logger.debug("WOAH, ERROR: recd has no thumbFilename?! " + str(recd) ) recd.savedXml = True - -def _saveMediaToDatastore( el, recd ): +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 != None): + 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): + 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): + 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: - recd.savedMedia = True - _saveXml( el, recd ) + + #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 @@ -296,38 +259,37 @@ def _saveMediaToDatastore( el, recd ): mediaObject.metadata['tags'] = recd.tags datastorePreviewPixbuf = recd.getThumbPixbuf() - if (recd.type == Constants.TYPE_AUDIO): + if recd.type == constants.TYPE_AUDIO: datastorePreviewPixbuf = recd.getAudioImagePixbuf() - elif (recd.type == Constants.TYPE_PHOTO): + elif recd.type == constants.TYPE_PHOTO: datastorePreviewFilepath = recd.getMediaFilepath() datastorePreviewPixbuf = gtk.gdk.pixbuf_new_from_file(datastorePreviewFilepath) datastorePreviewWidth = 300 datastorePreviewHeight = 225 - if (datastorePreviewPixbuf.get_width() != datastorePreviewWidth): + 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.hex) + "," + str(recd.colorFill.hex) + colors = str(recd.colorStroke) + "," + str(recd.colorFill) mediaObject.metadata['icon-color'] = colors - mtype = Constants.mediaTypes[recd.type] - mime = mtype[Constants.keyMime] - mediaObject.metadata['mime_type'] = mime + mtype = constants.MEDIA_INFO[recd.type] + mediaObject.metadata['mime_type'] = mtype['mime'] - mediaObject.metadata['activity_id'] = Constants.activityId + mediaObject.metadata['activity_id'] = activity._activity_id mediaFile = recd.getMediaFilepath() mediaObject.file_path = mediaFile mediaObject.transfer_ownership = True - datastore.write( mediaObject ) + datastore.write(mediaObject) recd.datastoreId = mediaObject.object_id recd.savedMedia = True - _saveXml( el, recd ) + _saveXml(el, recd) recd.mediaFilename = None @@ -21,12 +21,8 @@ import hippo import sugar from sugar.graphics import style -from sugar.graphics.palette import Palette, ToolInvoker -from sugar.graphics.toolbutton import ToolButton from sugar.graphics.icon import Icon -from constants import Constants - _PREVIOUS_PAGE = 0 _NEXT_PAGE = 1 @@ -148,8 +144,8 @@ class HTray(gtk.VBox): separator = hippo.Canvas() box = hippo.CanvasBox( - border_color=Constants.colorWhite.get_int(), - background_color=Constants.colorWhite.get_int(), + border_color=0xffffffff, + background_color=0xffffffff, box_height=1, border_bottom=1) separator.set_root(box) @@ -203,4 +199,4 @@ class HTray(gtk.VBox): return self._viewport.traybar.get_item_index(item) def scroll_to_end(self): - self._viewport._scroll_to_end()
\ No newline at end of file + self._viewport._scroll_to_end() @@ -1,2444 +0,0 @@ -#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 gtk -from gtk import gdk -from gtk import keysyms -import gobject -import cairo -import os -import pygst -pygst.require('0.10') -import gst -import gst.interfaces -import math -import shutil -import time -import pango -import hippo - -import logging -logger = logging.getLogger('record:ui.py') - -#from sugar.graphics.toolcombobox import ToolComboBox -#from sugar.graphics.tray import HTray -from sugar.graphics.toolbutton import ToolButton -from sugar import profile -from sugar import util -from sugar.activity import activity -from sugar.graphics import style - -from instance import Instance -from constants import Constants, istrMinutes, istrSeconds -from color import Color -from p5 import P5 -from p5_button import P5Button -from p5_button import Polygon -from p5_button import Button -import glive -from glive import LiveVideoWindow -from glivex import SlowLiveVideoWindow -from gplay import PlayVideoWindow -from recorded import Recorded -from button import RecdButton -import utils -import record -import aplay -from tray import HTray -from toolbarcombobox import ToolComboBox -import hw - -class UI: - - dim_THUMB_WIDTH = 108 - dim_THUMB_HEIGHT = 81 - dim_INSET = 10 - dim_PIPW = 160 - dim_PIPH = 120 #pipBorder - dim_PIP_BORDER = 4 - dim_PGDW = dim_PIPW + (dim_PIP_BORDER*2) - dim_PGDH = dim_PIPH + (dim_PIP_BORDER*2) - dim_CONTROLBAR_HT = 55 - - def __init__( self, pca ): - self.ca = pca - self.ACTIVE = False - self.LAUNCHING = True - self.ca.add_events(gtk.gdk.VISIBILITY_NOTIFY_MASK) - self.ca.connect("visibility-notify-event", self._visibleNotifyCb) - - self.inset = self.__class__.dim_INSET - self.pgdh = self.__class__.dim_PGDH - self.pgdw = self.__class__.dim_PGDW - self.thumbTrayHt = 150 #todo: get sugar constant here - self.thumbSvgW = 124 - self.thumbSvgH = 124 - self.maxw = 49 - self.maxh = 49 - self.controlBarHt = 60 - self.recordButtWd = 55 - self.pipw = self.__class__.dim_PIPW - self.piph = self.__class__.dim_PIPH - - #ui modes - - # True when we're in full-screen mode, False otherwise - self.FULLSCREEN = False - - # True when we're showing live video feed in the primary screen - # area, False otherwise (even when we are still showing live video - # in a p-i-p) - self.LIVEMODE = True - - self.LAST_MODE = -1 - self.LAST_FULLSCREEN = False - self.LAST_LIVE = True - self.LAST_MESHING = False - self.LAST_RECD_INFO = False - self.LAST_TRANSCODING = False - self.TRANSCODING = False - self.MESHING = False - - # RECD_INFO_ON is True when the 'info' for a recording is being - # display on-screen (who recorded it, tags, etc), and False otherwise. - self.RECD_INFO_ON = False - - self.UPDATE_DURATION_ID = 0 - self.UPDATE_TIMER_ID = 0 - self.COUNTINGDOWN = False - - #init - self.mapped = False - self.centered = False - self.setup = False - - #prep for when to show - self.shownRecd = None - - #this includes the default sharing tab - self.toolbox = activity.ActivityToolbox(self.ca) - self.ca.set_toolbox(self.toolbox) - - if glive.camera_presents: - self.photoToolbar = PhotoToolbar() - self.photoToolbar.set_sensitive( False ) - self.toolbox.add_toolbar( Constants.istrPhoto, self.photoToolbar ) - - self.videoToolbar = VideoToolbar() - self.videoToolbar.set_sensitive( False ) - self.toolbox.add_toolbar( Constants.istrVideo, self.videoToolbar ) - - self.tbars = { Constants.MODE_PHOTO: 1, - Constants.MODE_VIDEO: 2, - Constants.MODE_AUDIO: 3 } - else: - self.photoToolbar = None - self.videoToolbar = None - self.tbars = { Constants.MODE_AUDIO: 1 } - self.ca.m.MODE = Constants.MODE_AUDIO - - self.audioToolbar = AudioToolbar() - self.audioToolbar.set_sensitive( False ) - self.toolbox.add_toolbar( Constants.istrAudio, self.audioToolbar ) - - self.toolbox.set_current_toolbar(self.tbars[self.ca.m.MODE]) - - self.toolbox.remove(self.toolbox._separator) - #taken directly from toolbox.py b/c I don't know how to mod the hongry hippo - separator = hippo.Canvas() - box = hippo.CanvasBox( - border_color=Constants.colorBlack.get_int(), - background_color=Constants.colorBlack.get_int(), - box_height=style.TOOLBOX_SEPARATOR_HEIGHT, - border_bottom=style.LINE_WIDTH) - separator.set_root(box) - self.toolbox.pack_start(separator, False) - self.toolbox.separator = separator - - self.TOOLBOX_SIZE_ALLOCATE_ID = self.toolbox.connect_after("size-allocate", self._toolboxSizeAllocateCb) - self.toolbox._notebook.set_property("can-focus", False) - self.toolbox.connect("current-toolbar-changed", self._toolbarChangeCb) - self.toolbox.show_all() - - def serialize(self): - data = {} - - if self.photoToolbar: - data['photo_timer'] = self.photoToolbar.timerCb.combo.get_active() - - if self.videoToolbar: - data['video_timer'] = self.videoToolbar.timerCb.combo.get_active() - data['video_duration'] = self.videoToolbar.durCb.combo.get_active() - data['video_quality'] = self.videoToolbar.quality.combo.get_active() - - data['audio_timer'] = self.audioToolbar.timerCb.combo.get_active() - data['audio_duration'] = self.audioToolbar.durCb.combo.get_active() - - return data - - def deserialize(self, data): - if self.photoToolbar: - self.photoToolbar.timerCb.combo.set_active( - data.get('photo_timer', 0)) - - if self.videoToolbar: - self.videoToolbar.timerCb.combo.set_active( - data.get('video_timer', 0)) - self.videoToolbar.durCb.combo.set_active( - data.get('video_duration', 0)) - self.videoToolbar.quality.combo.set_active( - data.get('video_quality', 0)) - - self.audioToolbar.timerCb.combo.set_active(data.get('audio_timer', 0)) - self.audioToolbar.durCb.combo.set_active(data.get('audio_duration')) - - def _toolboxSizeAllocateCb( self, widget, event ): - self.toolbox.disconnect( self.TOOLBOX_SIZE_ALLOCATE_ID) - - toolboxHt = self.toolbox.size_request()[1] - self.vh = gtk.gdk.screen_height()-(self.thumbTrayHt+toolboxHt+self.controlBarHt) - self.vw = int(self.vh/.75) - self.letterBoxW = (gtk.gdk.screen_width() - self.vw)/2 - self.letterBoxVW = (self.vw/2)-(self.inset*2) - self.letterBoxVH = int(self.letterBoxVW*.75) - - self.setUpWindows() - - #now that we know how big the toolbox is, we can layout more - gobject.idle_add( self.layout ) - - - def layout( self ): - self.mainBox = gtk.VBox() - self.ca.set_canvas(self.mainBox) - - topBox = gtk.HBox() - self.mainBox.pack_start(topBox, expand=True) - - leftFill = gtk.VBox() - leftFill.set_size_request( self.letterBoxW, -1 ) - self.leftFillBox = gtk.EventBox( ) - self.leftFillBox.modify_bg( gtk.STATE_NORMAL, Constants.colorBlack.gColor ) - leftFill.add( self.leftFillBox ) - - topBox.pack_start( leftFill, expand=True ) - - centerVBox = gtk.VBox() - centerVBox.modify_bg(gtk.STATE_NORMAL, Constants.colorBlack.gColor) - topBox.pack_start( centerVBox, expand=True ) - self.centerBox = gtk.EventBox() - self.centerBox.set_size_request(self.vw, -1) - self.centerBox.modify_bg(gtk.STATE_NORMAL, Constants.colorBlack.gColor) - centerVBox.pack_start( self.centerBox, expand=True ) - centerSizer = gtk.VBox() - centerSizer.set_size_request(self.vw, -1) - centerSizer.modify_bg(gtk.STATE_NORMAL, Constants.colorBlack.gColor) - self.centerBox.add(centerSizer) - - self.bottomCenter = gtk.EventBox() - self.bottomCenter.modify_bg(gtk.STATE_NORMAL, Constants.colorBlack.gColor) - self.bottomCenter.set_size_request(self.vw, self.controlBarHt) - centerVBox.pack_start( self.bottomCenter, expand=False ) - - #into the center box we can put this guy... - self.backgdCanvasBox = gtk.VBox() - self.backgdCanvasBox.modify_bg(gtk.STATE_NORMAL, Constants.colorBlack.gColor) - self.backgdCanvasBox.set_size_request(self.vw, -1) - self.backgdCanvas = PhotoCanvas() - self.backgdCanvas.set_size_request(self.vw, self.vh) - self.backgdCanvasBox.pack_start( self.backgdCanvas, expand=False ) - - #or this guy... - self.infoBox = gtk.EventBox() - self.infoBox.modify_bg( gtk.STATE_NORMAL, Constants.colorButton.gColor ) - iinfoBox = gtk.VBox(spacing=self.inset) - self.infoBox.add( iinfoBox ) - iinfoBox.set_size_request(self.vw, -1) - iinfoBox.set_border_width(self.inset) - - rightFill = gtk.VBox() - rightFill.set_size_request( self.letterBoxW, -1 ) - rightFillBox = gtk.EventBox() - rightFillBox.modify_bg( gtk.STATE_NORMAL, Constants.colorBlack.gColor ) - rightFill.add( rightFillBox ) - topBox.pack_start( rightFill, expand=True ) - - #info box innards: - self.infoBoxTop = gtk.HBox() - iinfoBox.pack_start( self.infoBoxTop, expand=True ) - self.infoBoxTopLeft = gtk.VBox(spacing=self.inset) - self.infoBoxTop.pack_start( self.infoBoxTopLeft ) - self.infoBoxTopRight = gtk.VBox() - self.infoBoxTopRight.set_size_request(self.letterBoxVW, -1) - self.infoBoxTop.pack_start( self.infoBoxTopRight ) - - self.namePanel = gtk.HBox() - leftInfBalance = gtk.VBox() - self.nameLabel = gtk.Label("<b><span foreground='white'>"+Constants.istrTitle+"</span></b>") - self.nameLabel.set_use_markup( True ) - self.namePanel.pack_start( self.nameLabel, expand=False, padding=self.inset ) - self.nameLabel.set_alignment(0, .5) - self.nameTextfield = gtk.Entry(140) - self.nameTextfield.modify_bg( gtk.STATE_INSENSITIVE, Constants.colorBlack.gColor ) - self.nameTextfield.connect('changed', self._nameTextfieldEditedCb ) - self.nameTextfield.set_alignment(0) - self.nameTextfield.set_size_request( -1, self.controlBarHt-(self.inset*2) ) - self.namePanel.pack_start(self.nameTextfield) - - self.photographerPanel = gtk.VBox(spacing=self.inset) - self.infoBoxTopLeft.pack_start(self.photographerPanel, expand=False) - photographerLabel = gtk.Label("<b>" + Constants.istrRecorder + "</b>") - photographerLabel.set_use_markup( True ) - self.photographerPanel.pack_start(photographerLabel, expand=False) - photographerLabel.set_alignment(0, .5) - photoNamePanel = gtk.HBox(spacing=self.inset) - self.photographerPanel.pack_start(photoNamePanel) - - self.photoXoPanel = xoPanel() - photoNamePanel.pack_start( self.photoXoPanel, expand=False ) - self.photoXoPanel.set_size_request( 40, 40 ) - - self.photographerNameLabel = gtk.Label("") - self.photographerNameLabel.set_alignment(0, .5) - photoNamePanel.pack_start(self.photographerNameLabel) - - self.datePanel = gtk.HBox(spacing=self.inset) - self.infoBoxTopLeft.pack_start(self.datePanel, expand=False) - dateLabel = gtk.Label("<b>"+Constants.istrDate+"</b>") - dateLabel.set_use_markup(True) - self.datePanel.pack_start(dateLabel, expand=False) - self.dateDateLabel = gtk.Label("") - self.dateDateLabel.set_alignment(0, .5) - self.datePanel.pack_start(self.dateDateLabel) - - self.tagsPanel = gtk.VBox(spacing=self.inset) - tagsLabel = gtk.Label("<b>"+Constants.istrTags+"</b>") - tagsLabel.set_use_markup(True) - tagsLabel.set_alignment(0, .5) - self.tagsPanel.pack_start(tagsLabel, expand=False) - self.tagsBuffer = gtk.TextBuffer() - self.tagsBuffer.connect('changed', self._tagsBufferEditedCb) - self.tagsField = gtk.TextView(self.tagsBuffer) - self.tagsField.set_size_request( 100, 100 ) - self.tagsPanel.pack_start(self.tagsField, expand=True) - self.infoBoxTopLeft.pack_start(self.tagsPanel, expand=True) - - infoBotBox = gtk.HBox() - infoBotBox.set_size_request( -1, self.pgdh+self.inset ) - iinfoBox.pack_start(infoBotBox, expand=False) - - thumbnailsEventBox = gtk.EventBox() - thumbnailsEventBox.set_size_request( -1, self.thumbTrayHt ) - thumbnailsBox = gtk.HBox( ) - thumbnailsEventBox.add( thumbnailsBox ) - - self.thumbTray = HTray() - self.thumbTray.set_size_request( -1, self.thumbTrayHt ) - self.mainBox.pack_end( self.thumbTray, expand=False ) - self.thumbTray.show() - - self.CENTER_SIZE_ALLOCATE_ID = self.centerBox.connect_after("size-allocate", self._centerSizeAllocateCb) - self.ca.show_all() - - - def _centerSizeAllocateCb( self, widget, event ): - #initial setup of the panels - self.centerBox.disconnect(self.CENTER_SIZE_ALLOCATE_ID) - self.centerBoxPos = self.centerBox.translate_coordinates( self.ca, 0, 0 ) - - centerKid = self.centerBox.get_child() - if (centerKid != None): - self.centerBox.remove( centerKid ) - - self.centered = True - self.setUp() - - - def _mapEventCb( self, widget, event ): - #when your parent window is ready, turn on the feed of live video - self.liveVideoWindow.disconnect(self.MAP_EVENT_ID) - self.mapped = True - self.setUp() - - - def setUp( self ): - if (self.mapped and self.centered and not self.setup): - self.setup = True - - #set correct window sizes - self.setUpWindowsSizes() - - #listen for ctrl+c & game key buttons - self.ca.connect('key-press-event', self._keyPressEventCb) - #overlay widgets can go away after they've been on screen for a while - self.HIDE_WIDGET_TIMEOUT_ID = 0 - self.hiddenWidgets = False - self.resetWidgetFadeTimer() - self.showLiveVideoTags() - - if self.photoToolbar: - self.photoToolbar.set_sensitive( True ) - if self.videoToolbar: - self.videoToolbar.set_sensitive( True ) - self.audioToolbar.set_sensitive( True ) - - #initialize the app with the default thumbs - self.ca.m.setupMode( self.ca.m.MODE, True ) - - gobject.idle_add( self.finalSetUp ) - - - def finalSetUp( self ): - self.LAUNCHING = False - self.ACTIVE = self.ca.get_property( "visible" ) - self.updateVideoComponents() - - if (self.ACTIVE): - self.ca.glive.play() - - - def setUpWindows( self ): - #image windows - self.windowStack = [] - - #live video windows - self.livePhotoWindow = gtk.Window() - self.livePhotoWindow.modify_bg( gtk.STATE_NORMAL, Constants.colorBlack.gColor ) - self.livePhotoWindow.modify_bg( gtk.STATE_INSENSITIVE, Constants.colorBlack.gColor ) - self.addToWindowStack( self.livePhotoWindow, self.ca ) - self.livePhotoCanvas = PhotoCanvas() - self.livePhotoWindow.add(self.livePhotoCanvas) - self.livePhotoWindow.connect("button_release_event", self._mediaClickedForPlayback) - self.livePhotoWindow.add_events(gtk.gdk.VISIBILITY_NOTIFY_MASK) - self.livePhotoWindow.connect("visibility-notify-event", self._visibleNotifyCb) - - #video playback windows - self.playOggWindow = PlayVideoWindow(Constants.colorBlack.gColor) - self.addToWindowStack( self.playOggWindow, self.windowStack[len(self.windowStack)-1] ) - #self.playOggWindow.set_gplay(self.ca.gplay) - self.ca.gplay.window = self.playOggWindow - self.playOggWindow.set_events(gtk.gdk.BUTTON_RELEASE_MASK) - self.playOggWindow.connect("button_release_event", self._mediaClickedForPlayback) - self.playOggWindow.add_events(gtk.gdk.VISIBILITY_NOTIFY_MASK) - self.playOggWindow.connect("visibility-notify-event", self._visibleNotifyCb) - - #border behind - self.pipBgdWindow = gtk.Window() - self.pipBgdWindow.modify_bg( gtk.STATE_NORMAL, Constants.colorWhite.gColor ) - self.pipBgdWindow.modify_bg( gtk.STATE_INSENSITIVE, Constants.colorWhite.gColor ) - self.addToWindowStack( self.pipBgdWindow, self.windowStack[len(self.windowStack)-1] ) - - self.liveVideoWindow = LiveVideoWindow(Constants.colorBlack.gColor) - self.addToWindowStack( self.liveVideoWindow, self.windowStack[len(self.windowStack)-1] ) - self.liveVideoWindow.set_glive(self.ca.glive) - self.liveVideoWindow.set_events(gtk.gdk.BUTTON_RELEASE_MASK) - self.liveVideoWindow.connect("button_release_event", self._liveButtonReleaseCb) - self.liveVideoWindow.add_events(gtk.gdk.VISIBILITY_NOTIFY_MASK) - self.liveVideoWindow.connect("visibility-notify-event", self._visibleNotifyCb) - - self.slowLiveVideoWindow = SlowLiveVideoWindow(Constants.colorBlack.gColor) - self.addToWindowStack( self.slowLiveVideoWindow, self.windowStack[len(self.windowStack)-1] ) - self.slowLiveVideoWindow.set_glivex(self.ca.glivex) - self.slowLiveVideoWindow.set_events(gtk.gdk.BUTTON_RELEASE_MASK) - self.slowLiveVideoWindow.connect("button_release_event", self._liveButtonReleaseCb) - self.slowLiveVideoWindow.add_events(gtk.gdk.VISIBILITY_NOTIFY_MASK) - self.slowLiveVideoWindow.connect("visibility-notify-event", self._visibleNotifyCb) - - self.recordWindow = RecordWindow(self) - self.addToWindowStack( self.recordWindow, self.windowStack[len(self.windowStack)-1] ) - - self.progressWindow = ProgressWindow(self) - self.addToWindowStack( self.progressWindow, self.windowStack[len(self.windowStack)-1] ) - - self.maxWindow = gtk.Window() - self.maxWindow.modify_bg( gtk.STATE_NORMAL, Constants.colorBlack.gColor ) - self.maxWindow.modify_bg( gtk.STATE_INSENSITIVE, Constants.colorBlack.gColor ) - maxButton = MaxButton(self) - self.maxWindow.add( maxButton ) - self.addToWindowStack( self.maxWindow, self.windowStack[len(self.windowStack)-1] ) - - self.scrubWindow = ScrubberWindow(self) - self.addToWindowStack( self.scrubWindow, self.windowStack[len(self.windowStack)-1] ) - - self.infWindow = gtk.Window() - self.infWindow.modify_bg( gtk.STATE_NORMAL, Constants.colorBlack.gColor ) - self.infWindow.modify_bg( gtk.STATE_INSENSITIVE, Constants.colorBlack.gColor ) - infButton= InfButton(self) - self.infWindow.add(infButton) - self.addToWindowStack( self.infWindow, self.windowStack[len(self.windowStack)-1] ) - - self.hideAllWindows() - self.MAP_EVENT_ID = self.liveVideoWindow.connect_after("map-event", self._mapEventCb) - for i in range (0, len(self.windowStack)): -# self.windowStack[i].add_events(gtk.gdk.VISIBILITY_NOTIFY_MASK) -# self.windowStack[i].connect("visibility-notify-event", self._visibleNotifyCb) - self.windowStack[i].show_all() - - - def _visibleNotifyCb( self, widget, event ): - - if (self.LAUNCHING): - return - - temp_ACTIVE = True - - if (event.state == gtk.gdk.VISIBILITY_FULLY_OBSCURED): - - if (not self.FULLSCREEN): - if (widget == self.ca): - temp_ACTIVE = False - - else: - if (self.ca.m.MODE == Constants.MODE_PHOTO): - if (not self.LIVEMODE and widget == self.livePhotoWindow): - temp_ACTIVE = False - if ( self.LIVEMODE and widget == self.liveVideoWindow): - temp_ACTIVE = False - - if (self.ca.m.MODE == Constants.MODE_VIDEO): - if (not self.LIVEMODE and widget == self.playOggWindow): - temp_ACTIVE = False - if ( self.LIVEMODE and widget == self.liveVideoWindow): - temp_ACTIVE = False - - - if (temp_ACTIVE != self.ACTIVE): - self.ACTIVE = temp_ACTIVE - if (self.ACTIVE): - self.ca.restartPipes() - else: - self.ca.stopPipes() - - - def setUpWindowsSizes( self ): - pipDim = self.getPipDim(False) - eyeDim = self.getEyeDim(False) - imgDim = self.getImgDim( False ) - pgdDim = self.getPgdDim( False ) - maxDim = self.getMaxDim( False ) - prgDim = self.getPrgDim( False ) - infDim = self.getInfDim( False ) - self.livePhotoWindow.resize( imgDim[0], imgDim[1] ) - self.pipBgdWindow.resize( pgdDim[0], pgdDim[1] ) - self.liveVideoWindow.resize( imgDim[0], imgDim[1] ) - self.playOggWindow.resize( imgDim[0], imgDim[1] ) - self.recordWindow.resize( eyeDim[0], eyeDim[1] ) - self.maxWindow.resize( maxDim[0], maxDim[1] ) - self.progressWindow.resize( prgDim[0], prgDim[1] ) - self.infWindow.resize( infDim[0], infDim[1] ) - - - def _toolbarChangeCb( self, tbox, num ): - if (num != 0) and (self.ca.m.RECORDING or self.ca.m.UPDATING): - self.toolbox.set_current_toolbar(self.tbars[self.ca.m.MODE]) - else: - mode = [mode for mode, i in self.tbars.items() if i == num] - if not mode: - return - if (mode[0] == Constants.MODE_PHOTO) and \ - (self.ca.m.MODE != Constants.MODE_PHOTO): - self.ca.m.doPhotoMode() - elif(mode[0] == Constants.MODE_VIDEO) and \ - (self.ca.m.MODE != Constants.MODE_VIDEO): - self.ca.m.doVideoMode() - elif(mode[0] == Constants.MODE_AUDIO) and \ - (self.ca.m.MODE != Constants.MODE_AUDIO): - self.ca.m.doAudioMode() - - - def addToWindowStack( self, win, parent ): - self.windowStack.append( win ) - win.set_transient_for( parent ) - win.set_type_hint( gtk.gdk.WINDOW_TYPE_HINT_DIALOG ) - win.set_decorated( False ) - win.set_focus_on_map( False ) - win.set_property("accept-focus", False) - win.props.destroy_with_parent = True - - - def resetWidgetFadeTimer( self ): - #only show the clutter when the mouse moves - self.mx = -1 - self.my = -1 - self.hideWidgetsTimer = time.time() - if (self.hiddenWidgets): - self.showWidgets() - self.hiddenWidgets = False - - #remove, then add - self.doMouseListener( False ) - if (self.HIDE_WIDGET_TIMEOUT_ID != 0): - gobject.source_remove( self.HIDE_WIDGET_TIMEOUT_ID) - - self.HIDE_WIDGET_TIMEOUT_ID = gobject.timeout_add( 500, self._mouseMightaMovedCb ) - - - def doMouseListener( self, listen ): - if (listen): - self.resetWidgetFadeTimer() - else: - if (self.HIDE_WIDGET_TIMEOUT_ID != None): - if (self.HIDE_WIDGET_TIMEOUT_ID != 0): - gobject.source_remove( self.HIDE_WIDGET_TIMEOUT_ID ) - - - def hideWidgets( self ): - self.moveWinOffscreen( self.maxWindow ) - self.moveWinOffscreen( self.pipBgdWindow ) - self.moveWinOffscreen( self.infWindow ) - self.moveWinOffscreen( self.slowLiveVideoWindow ) - - if (self.FULLSCREEN): - self.moveWinOffscreen( self.recordWindow ) - self.moveWinOffscreen( self.progressWindow ) - self.moveWinOffscreen( self.scrubWindow ) - - if (self.ca.m.MODE == Constants.MODE_PHOTO): - if (not self.LIVEMODE): - self.moveWinOffscreen( self.liveVideoWindow ) - elif (self.ca.m.MODE == Constants.MODE_VIDEO): - if (not self.LIVEMODE): - self.moveWinOffscreen( self.liveVideoWindow ) - elif (self.ca.m.MODE == Constants.MODE_AUDIO): - if (not self.LIVEMODE): - self.moveWinOffscreen( self.liveVideoWindow ) - self.LAST_MODE = -1 - - - def _mouseMightaMovedCb( self ): - x, y = self.ca.get_pointer() - passedTime = 0 - - if (x != self.mx or y != self.my): - self.hideWidgetsTimer = time.time() - if (self.hiddenWidgets): - self.showWidgets() - self.hiddenWidgets = False - else: - passedTime = time.time() - self.hideWidgetsTimer - - if (self.ca.m.RECORDING): - self.hideWidgetsTimer = time.time() - passedTime = 0 - - if (passedTime >= 3): - if (not self.hiddenWidgets): - if (self.mouseInWidget(x,y)): - self.hideWidgetsTimer = time.time() - elif (self.RECD_INFO_ON): - self.hideWidgetsTimer = time.time() - elif (self.UPDATE_TIMER_ID != 0): - self.hideWidgetsTimer = time.time() - else: - self.hideWidgets() - self.hiddenWidgets = True - - self.mx = x - self.my = y - return True - - - def mouseInWidget( self, mx, my ): - if (self.ca.m.MODE != Constants.MODE_AUDIO): - if (self.inWidget( mx, my, self.getLoc("max", self.FULLSCREEN), self.getDim("max", self.FULLSCREEN))): - return True - - if (not self.LIVEMODE): - if (self.inWidget( mx, my, self.getLoc("pgd", self.FULLSCREEN), self.getDim("pgd", self.FULLSCREEN))): - return True - - if (self.inWidget( mx, my, self.getLoc("inb", self.FULLSCREEN), self.getDim("inb", self.FULLSCREEN))): - return True - - if (self.inWidget( mx, my, self.getLoc("prg", self.FULLSCREEN), self.getDim("prg", self.FULLSCREEN))): - return True - - if (self.inWidget( mx, my, self.getLoc("inf", self.FULLSCREEN), self.getDim("inf", self.FULLSCREEN))): - return True - - if (self.LIVEMODE): - if (self.inWidget( mx, my, self.getLoc("eye", self.FULLSCREEN), self.getDim("eye", self.FULLSCREEN))): - return True - - return False - - - def _mediaClickedForPlayback(self, widget, event): - if (not self.LIVEMODE): - if (self.shownRecd != None): - if (self.ca.m.MODE != Constants.MODE_PHOTO): - self.showThumbSelection( self.shownRecd ) - - - def inWidget( self, mx, my, loc, dim ): - if ( (mx > loc[0]) and (my > loc[1]) ): - if ( (mx < loc[0]+dim[0]) and (my < loc[1]+dim[1]) ): - return True - - - def _nameTextfieldEditedCb(self, widget): - if (self.shownRecd != None): - if (self.nameTextfield.get_text() != self.shownRecd.title): - self.shownRecd.setTitle( self.nameTextfield.get_text() ) - - - def _tagsBufferEditedCb(self, widget): - if (self.shownRecd != None): - txt = self.tagsBuffer.get_text( self.tagsBuffer.get_start_iter(), self.tagsBuffer.get_end_iter() ) - if (txt != self.shownRecd.tags): - self.shownRecd.setTags( txt ) - - - def _keyPressEventCb( self, widget, event): - #todo: trac #4144 - - self.resetWidgetFadeTimer() - - #we listen here for CTRL+C events and game keys, and pass on events to gtk.Entry fields - keyname = gtk.gdk.keyval_name(event.keyval) - - if (keyname == 'KP_Page_Up'): #O, up - if (self.LIVEMODE): - if (not self.ca.m.UPDATING): - self.doShutter() - else: - if (self.COUNTINGDOWN): - self.doShutter() - else: - if (self.ca.m.MODE == Constants.MODE_PHOTO): - self.resumeLiveVideo() - else: - self.resumePlayLiveVideo() - elif (keyname == 'KP_Page_Down'): #x, down - if (not self.ca.m.UPDATING and not self.ca.m.RECORDING): - self.ca.m.showLastThumb() - elif (keyname == 'KP_Home'): #square, left - if (not self.ca.m.UPDATING and not self.ca.m.RECORDING and not self.LIVEMODE): - self.ca.m.showPrevThumb( self.shownRecd ) - elif (keyname == 'KP_End'): #check, right - if (not self.ca.m.UPDATING and not self.ca.m.RECORDING and not self.LIVEMODE): - self.ca.m.showNextThumb( self.shownRecd ) - elif (keyname == 'c' and event.state == gtk.gdk.CONTROL_MASK): - if (self.shownRecd != None): - self.copyToClipboard( self.shownRecd ) - elif (keyname == 'Escape'): - if (self.FULLSCREEN): - self.FULLSCREEN = False - if (self.RECD_INFO_ON): - self.infoButtonClicked() - else: - self.updateVideoComponents() - elif (keyname == 'i' and event.state == gtk.gdk.CONTROL_MASK): - if (not self.LIVEMODE): - self.infoButtonClicked() - - return False - - - def copyToClipboard( self, recd ): - if (recd.isClipboardCopyable( )): - tmpImgPath = self.doClipboardCopyStart( recd ) - gtk.Clipboard().set_with_data( [('text/uri-list', 0, 0)], self._clipboardGetFuncCb, self._clipboardClearFuncCb, tmpImgPath ) - return True - - - def doClipboardCopyStart( self, recd ): - imgPath_s = recd.getMediaFilepath() - if (imgPath_s == None): - record.Record.log.error("doClipboardCopyStart: imgPath_s==None") - return None - - tmpImgPath = recd.getMediaFilepath() - tmpImgPath = utils.getUniqueFilepath(tmpImgPath, 0) - shutil.copyfile( imgPath_s, tmpImgPath ) - return tmpImgPath - - - def doClipboardCopyCopy( self, tmpImgPath, selection_data ): - tmpImgUri = "file://" + tmpImgPath - selection_data.set( "text/uri-list", 8, tmpImgUri ) - - - def doClipboardCopyFinish( self, tmpImgPath ): - if (tmpImgPath != None): - if (os.path.exists(tmpImgPath)): - os.remove( tmpImgPath ) - tmpImgPath = None - - - def _clipboardGetFuncCb( self, clipboard, selection_data, info, data): - self.doClipboardCopyCopy( data, selection_data ) - - - def _clipboardClearFuncCb( self, clipboard, data): - self.doClipboardCopyFinish( data ) - - - def showPhoto( self, recd ): - pixbuf = self.getPhotoPixbuf( recd ) - if (pixbuf != None): - #self.shownRecd = recd - - img = hippo.cairo_surface_from_gdk_pixbuf(pixbuf) - self.livePhotoCanvas.setImage( img ) - - self.LIVEMODE = False - self.updateVideoComponents() - - self.showRecdMeta(recd) - - - def getPhotoPixbuf( self, recd ): - pixbuf = None - - downloading = self.ca.requestMeshDownload(recd) - self.MESHING = downloading - - if (not downloading): - self.progressWindow.updateProgress(0, "") - imgPath = recd.getMediaFilepath() - if (not imgPath == None): - if ( os.path.isfile(imgPath) ): - pixbuf = gtk.gdk.pixbuf_new_from_file(imgPath) - - if (pixbuf == None): - #maybe it is not downloaded from the mesh yet... - #but we can show the low res thumb in the interim - pixbuf = recd.getThumbPixbuf() - - return pixbuf - - - def showLiveVideoTags( self ): - self.shownRecd = None - self.livePhotoCanvas.setImage( None ) - self.nameTextfield.set_text("") - self.tagsBuffer.set_text("") - - self.scrubWindow.removeCallbacks() - self.scrubWindow.reset() - self.MESHING = False - self.progressWindow.updateProgress( 0, "" ) - - self.resetWidgetFadeTimer( ) - - - def updateButtonSensitivities( self ): - switchStuff = ((not self.ca.m.UPDATING) and (not self.ca.m.RECORDING)) - - if self.photoToolbar: - self.photoToolbar.set_sensitive( switchStuff ) - if self.videoToolbar: - self.videoToolbar.set_sensitive( switchStuff ) - self.audioToolbar.set_sensitive( switchStuff ) - - if (not self.COUNTINGDOWN): - if (self.ca.m.UPDATING): - self.ca.ui.setWaitCursor( self.ca.window ) - for i in range (0, len(self.windowStack)): - self.ca.ui.setWaitCursor( self.windowStack[i].window ) - else: - self.ca.ui.setDefaultCursor( self.ca.window ) - for i in range (0, len(self.windowStack)): - self.ca.ui.setDefaultCursor( self.windowStack[i].window ) - - #display disc is full messages - self.ca.m.updateXoFullStatus() - self.recordWindow.displayDiscFullText(self.ca.m.FULL) - if (self.ca.m.FULL): - self.recordWindow.shutterButton.set_sensitive( False, True) - fullMessage = Constants.istrYourDiskIsFull % {"1":Constants.istrJournal} - self.progressWindow.updateProgress( 1, fullMessage, "gray" ) - else: - self.recordWindow.shutterButton.set_sensitive( not self.ca.m.UPDATING, False ) - if (self.ca.m.RECORDING): - self.recordWindow.shutterButton.doRecordButton() - else: - self.recordWindow.shutterButton.doNormalButton() - - kids = self.thumbTray.get_children() - for i in range (0, len(kids)): - if (self.ca.m.UPDATING or self.ca.m.RECORDING): - if (kids[i].getButtClickedId() != 0): - kids[i].disconnect( kids[i].getButtClickedId() ) - kids[i].setButtClickedId(0) - else: - if (kids[i].getButtClickedId() == 0): - BUTT_CLICKED_ID = kids[i].connect( "clicked", self._thumbClicked, kids[i].recd ) - kids[i].setButtClickedId(BUTT_CLICKED_ID) - - - def hideAllWindows( self ): - for i in range (0, len(self.windowStack)): - self.moveWinOffscreen( self.windowStack[i] ) - - - def _liveButtonReleaseCb(self, widget, event): - self.ca.gplay.stop() - self.ca.glivex.stop() - self.ca.glive.play() - self.resumeLiveVideo() - - - def resumeLiveVideo( self ): - self.livePhotoCanvas.setImage( None ) - - bottomKid = self.bottomCenter.get_child() - if (bottomKid != None): - self.bottomCenter.remove( bottomKid ) - - self.RECD_INFO_ON = False - - if (not self.LIVEMODE): - self.ca.m.setUpdating(True) - self.ca.gplay.stop() - self.showLiveVideoTags() - self.LIVEMODE = True - self.updateVideoComponents() - self.ca.m.setUpdating(False) - - - def _playLiveButtonReleaseCb(self, widget, event): - self.resumePlayLiveVideo() - - - def resumePlayLiveVideo( self ): - self.ca.gplay.stop() - - self.RECD_INFO_ON = False - #if you are big on the screen, don't go changing anything, ok? - if (self.LIVEMODE): - return - - self.showLiveVideoTags() - self.LIVEMODE = True - self.startLiveVideo() - self.updateVideoComponents() - - - def recordVideo( self ): - self.ca.glive.startRecordingVideo(self.videoToolbar.getQuality()) - self.beginRecordingTimer( ) - - - def recordAudio( self ): - self.ca.glive.startRecordingAudio( ) - self.beginRecordingTimer( ) - - - def beginRecordingTimer( self ): - self.recTime = time.time() - self.UPDATE_DURATION_ID = gobject.timeout_add( 500, self._updateDurationCb ) - - - def _updateDurationCb( self ): - passedTime = time.time() - self.recTime - - duration = 10.0 - if (self.ca.m.MODE == Constants.MODE_VIDEO): - duration = self.videoToolbar.getDuration()+0.0 - elif (self.ca.m.MODE == Constants.MODE_AUDIO): - duration = self.audioToolbar.getDuration()+0.0 - - if (passedTime >= duration ): - self.completeCountdown() - self.progressWindow.updateProgress( 1, Constants.istrFinishedRecording ) - if (self.ca.m.RECORDING): - gobject.idle_add( self.doShutter ) - - return False - else: - secsRemaining = duration - passedTime - if (secsRemaining >= 60): - mins = int( secsRemaining/60 ) - secs = int( secsRemaining%60 ) - timeRemainStr = istrMinutes(mins) + ', ' + istrSeconds(secs) - else: - timeRemainStr = istrSeconds(secsRemaining) - - self.progressWindow.updateProgress( passedTime/duration, - Constants.istrRemaining + " " + timeRemainStr ) - return True - - - def completeCountdown( self ): - if (self.UPDATE_DURATION_ID != 0): - gobject.source_remove( self.UPDATE_DURATION_ID ) - self.UPDATE_DURATION_ID = 0 - - - def updateModeChange(self): - #this is called when a menubar button is clicked - self.LIVEMODE = True - self.FULLSCREEN = False - self.RECD_INFO_ON = False - self.MESHING = False - - self.progressWindow.updateProgress(0, "") - - #set up the x & xv x-ition (if need be) - self.ca.gplay.stop() - self.startLiveVideo() - - bottomKid = self.bottomCenter.get_child() - if (bottomKid != None): - self.bottomCenter.remove( bottomKid ) - - self.doMouseListener( True ) - self.showLiveVideoTags() - self.LAST_MODE = -1 #force an update - self.updateVideoComponents() - self.resetWidgetFadeTimer() - - - def startLiveVideo(self): - self.ca.glivex.stop() - self.ca.glive.play() - - - def doFullscreen( self ): - self.FULLSCREEN = not self.FULLSCREEN - self.updateVideoComponents() - - - def moveWinOffscreen( self, win ): - #we move offscreen to resize or else we get flashes on screen, and setting hide() doesn't allow resize & moves - offW = (gtk.gdk.screen_width() + 100) - offH = (gtk.gdk.screen_height() + 100) - self.smartMove(win, offW, offH) - - - def setImgLocDim( self, win ): - imgDim = self.getImgDim( self.FULLSCREEN ) - self.smartResize( win, imgDim[0], imgDim[1] ) - imgLoc = self.getImgLoc( self.FULLSCREEN ) - self.smartMove( win, imgLoc[0], imgLoc[1] ) - - - def setPrgLocDim( self, win ): - prgDim = self.getPrgDim( self.FULLSCREEN ) - self.smartResize( win, prgDim[0], prgDim[1] ) - prgLoc = self.getPrgLoc( self.FULLSCREEN ) - self.smartMove( win, prgLoc[0], prgLoc[1] ) - - - def setTmrLocDim( self, win ): - tmrDim = self.getTmrDim( self.FULLSCREEN ) - self.smartResize( win, tmrDim[0], tmrDim[1] ) - tmrLoc = self.getTmrLoc( self.FULLSCREEN ) - self.smartMove( win, tmrLoc[0], tmrLoc[1] ) - - - def setScrLocDim( self, win ): - scrDim = self.getScrDim( self.FULLSCREEN ) - self.smartResize( win, scrDim[0], scrDim[1] ) - scrLoc = self.getScrLoc( self.FULLSCREEN ) - self.smartMove( win, scrLoc[0], scrLoc[1] ) - - - def setInfLocDim( self, win ): - infDim = self.getInfDim( self.FULLSCREEN ) - self.smartResize( win, infDim[0], infDim[1] ) - infLoc = self.getInfLoc( self.FULLSCREEN ) - self.smartMove( win, infLoc[0], infLoc[1] ) - - - def getScrDim( self, full ): - if (full): - return [gtk.gdk.screen_width()-(self.inset+self.pgdw+self.inset+self.inset), self.controlBarHt] - else: - return [self.vw, self.controlBarHt] - - - def getScrLoc( self, full ): - if (full): - return [(self.inset+self.pgdw+self.inset), gtk.gdk.screen_height()-(self.inset+self.controlBarHt)] - else: - return [self.centerBoxPos[0], self.centerBoxPos[1]+self.vh] - - - def getImgDim( self, full ): - if (full): - return [gtk.gdk.screen_width(), gtk.gdk.screen_height()] - else: - return [self.vw, self.vh] - - - def getImgLoc( self, full ): - if (full): - return[0, 0] - else: - return[self.centerBoxPos[0], self.centerBoxPos[1]] - - - def getTmrLoc( self, full ): - if (not full): - return [self.centerBoxPos[0], self.centerBoxPos[1]+self.vh] - else: - return [self.inset, gtk.gdk.screen_height()-(self.inset+self.controlBarHt)] - - - def getTmrDim( self, full ): - if (not full): - return [self.vw, self.controlBarHt] - else: - return [gtk.gdk.screen_width()-(self.inset+self.inset), self.controlBarHt] - - - def setPipLocDim( self, win ): - self.smartResize( win, self.pipw, self.piph ) - - loc = self.getPipLoc( self.FULLSCREEN ) - self.smartMove( win, loc[0], loc[1] ) - - - def getPipLoc( self, full ): - if (full): - return [self.inset+self.__class__.dim_PIP_BORDER, gtk.gdk.screen_height()-(self.inset+self.piph+self.__class__.dim_PIP_BORDER)] - else: - return [self.centerBoxPos[0]+self.inset+self.__class__.dim_PIP_BORDER, (self.centerBoxPos[1]+self.vh)-(self.inset+self.piph+self.__class__.dim_PIP_BORDER)] - - - def setPipBgdLocDim( self, win ): - pgdLoc = self.getPgdLoc( self.FULLSCREEN ) - self.smartMove( win, pgdLoc[0], pgdLoc[1] ) - - - def getPgdLoc( self, full ): - if (full): - return [self.inset, gtk.gdk.screen_height()-(self.inset+self.pgdh)] - else: - return [self.centerBoxPos[0]+self.inset, (self.centerBoxPos[1]+self.vh)-(self.inset+self.pgdh)] - - - def setMaxLocDim( self, win ): - maxLoc = self.getMaxLoc( self.FULLSCREEN ) - self.smartMove( win, maxLoc[0], maxLoc[1] ) - - - def getMaxLoc( self, full ): - if (full): - return [gtk.gdk.screen_width()-(self.maxw+self.inset), self.inset] - else: - return [(self.centerBoxPos[0]+self.vw)-(self.inset+self.maxw), self.centerBoxPos[1]+self.inset] - - - def getInfLoc( self, full ): - if (full): - return [gtk.gdk.screen_width()+100,gtk.gdk.screen_height()+100 ] - else: - dim = self.getInfDim(self.FULLSCREEN) - return [(self.centerBoxPos[0]+self.vw)-dim[0], (self.centerBoxPos[1]+self.vh)-dim[1]] - - - def setEyeLocDim( self, win ): - dim = self.getEyeDim( self.FULLSCREEN ) - self.smartResize( win, dim[0], dim[1] ) - loc = self.getEyeLoc( self.FULLSCREEN ) - self.smartMove( win, loc[0], loc[1] ) - - - def getEyeLoc( self, full ): - if (not full): - return [self.centerBoxPos[0], self.centerBoxPos[1]+self.vh] - else: - return [self.inset, gtk.gdk.screen_height()-(self.inset+self.controlBarHt)] - - - def getEyeDim( self, full ): - if (not full): - if (self.ca.m.MODE == Constants.MODE_PHOTO): - return [self.vw, self.controlBarHt] - else: - return [self.recordButtWd, self.controlBarHt] - else: - if (self.ca.m.MODE == Constants.MODE_PHOTO): - return [gtk.gdk.screen_width()-(self.inset*2), self.controlBarHt] - else: - return [self.recordButtWd, self.controlBarHt] - - - def getInbLoc( self, full ): - return [(self.centerBoxPos[0]+self.vw)-(self.inset+self.letterBoxVW), self.centerBoxPos[1]+self.inset] - - - def setInbLocDim( self, win ): - dim = self.getInbDim( self.FULLSCREEN ) - self.smartResize( win, dim[0], dim[1] ) - loc = self.getInbLoc(self.FULLSCREEN) - self.smartMove( win, loc[0], loc[1] ) - - - def smartResize( self, win, w, h ): - winSize = win.get_size() - if ( (winSize[0] != w) or (winSize[1] != h) ): - win.resize( w, h ) - return True - else: - return False - - - def smartMove( self, win, x, y ): - winLoc = win.get_position() - if ( (winLoc[0] != x) or (winLoc[1] != y) ): - win.move( x, y ) - return True - else: - return False - - - def getDim( self, pos, full ): - if (pos == "pip"): - return self.getPipDim( full ) - elif(pos == "pgd"): - return self.getPgdDim( full ) - elif(pos == "max"): - return self.getMaxDim( full ) - elif(pos == "img"): - return self.getImgDim( full ) - elif(pos == "eye"): - return self.getEyeDim( full ) - elif(pos == "inb"): - return self.getInbDim( full ) - elif(pos == "prg"): - return self.getPrgDim( full ) - elif(pos == "inf"): - return self.getInfDim( full ) - - - def getMaxDim( self, full ): - return [self.maxw, self.maxh] - - - def getInfDim( self, full ): - return [75, 75] - - - def getPipDim( self, full ): - return [self.pipw, self.piph] - - - def getPgdDim( self, full ): - return [self.pgdw, self.pgdh] - - - def getInbDim( self, full ): - return [self.letterBoxVW, self.letterBoxVH] - - - def getPrgDim( self, full ): - if (not full): - return [self.vw-self.recordButtWd, self.controlBarHt] - else: - return [gtk.gdk.screen_width()-(self.inset+self.inset+self.recordButtWd), self.controlBarHt] - - - def getPrgLoc( self, full ): - if (not full): - return [self.centerBoxPos[0]+self.recordButtWd, self.centerBoxPos[1]+self.vh] - else: - return [self.inset+self.recordButtWd, gtk.gdk.screen_height()-(self.inset+self.controlBarHt)] - - - def getLoc( self, pos, full ): - if (pos == "pip"): - return self.getPipLoc( full ) - elif(pos == "pgd"): - return self.getPgdLoc( full ) - elif(pos == "max"): - return self.getMaxLoc( full ) - elif(pos == "img"): - return self.getImgLoc( full ) - elif(pos == "eye"): - return self.getEyeLoc( full ) - elif(pos == "inb"): - return self.getInbLoc( full ) - elif(pos == "prg"): - return self.getPrgLoc( full ) - elif(pos == "inf"): - return self.getInfLoc( full ) - - - def _shutterClickCb( self, arg ): - self.doShutter() - - - def doShutter( self ): - if (self.UPDATE_TIMER_ID == 0): - if (not self.ca.m.RECORDING): - - self.ca.m.updateXoFullStatus() - if (self.ca.m.FULL): - self.updateButtonSensitivities() - return - - #there is no update timer running, so we need to find out if there is a timer needed - timerTime = 0 - if (self.ca.m.MODE == Constants.MODE_PHOTO): - timerTime = self.photoToolbar.getTimer() - elif (self.ca.m.MODE == Constants.MODE_VIDEO): - timerTime = self.videoToolbar.getTimer() - elif (self.ca.m.MODE == Constants.MODE_AUDIO): - timerTime = self.audioToolbar.getTimer() - - if (timerTime > 0): - self.timerStartTime = time.time() - self.UPDATE_TIMER_ID = gobject.timeout_add( 500, self._updateTimerCb ) - self.COUNTINGDOWN = True - self.ca.m.setUpdating(True) - else: - self.clickShutter() - else: - #or, if there is no countdown, it might be because we are recording - self.clickShutter() - - else: - #we're timing down something, but interrupted by user click or the timer completing - self.completeTimer() - gobject.idle_add( self.clickShutter ) - - - def completeTimer( self ): - self.COUNTINGDOWN = False - self.ca.m.setUpdating(False) - self.recordWindow.updateCountdown(-1) - self.progressWindow.updateProgress( 1, "" ) - gobject.source_remove( self.UPDATE_TIMER_ID ) - self.UPDATE_TIMER_ID = 0 - - - def _updateTimerCb( self ): - nowTime = time.time() - passedTime = nowTime - self.timerStartTime - - timerTime = 0 - if (self.ca.m.MODE == Constants.MODE_PHOTO): - timerTime = self.photoToolbar.getTimer() - elif (self.ca.m.MODE == Constants.MODE_VIDEO): - timerTime = self.videoToolbar.getTimer() - elif (self.ca.m.MODE == Constants.MODE_AUDIO): - timerTime = self.audioToolbar.getTimer() - - if (passedTime >= timerTime): - self.COUNTINGDOWN = False - self.ca.m.setUpdating(False) - self.doShutter() - return False - else: - secsRemaining = timerTime-passedTime - timeRemainStr = istrSeconds(secsRemaining) - self.progressWindow.updateProgress( passedTime/timerTime, - Constants.istrRemaining + " " + timeRemainStr) - self.recordWindow.updateCountdown( int(secsRemaining) ) - return True - - - def clickShutter( self ): - if self.ca.m.RECORDING: - self.ca.m.doShutter() - aplay.play(Constants.soundClick) - else: - aplay.play(Constants.soundClick, self.ca.m.doShutter) - - - def updateVideoComponents( self ): - logger.debug('updateVideoComponents: MODE=(%s,%s) FULLSCREEN=(%s,%s)' \ - ' LIVE=(%s,%s) RECD_INFO=(%s,%s) TRANSCODING=(%s,%s)' \ - ' MESHING=(%s,%s) windowStack=%s' \ - % (self.LAST_MODE, self.ca.m.MODE, - self.LAST_FULLSCREEN, self.FULLSCREEN, - self.LAST_LIVE, self.LIVEMODE, - self.LAST_RECD_INFO, self.RECD_INFO_ON, - self.LAST_TRANSCODING, self.TRANSCODING, - self.LAST_MESHING, self.MESHING, - len(self.windowStack))) - - if ( (self.LAST_MODE == self.ca.m.MODE) - and (self.LAST_FULLSCREEN == self.FULLSCREEN) - and (self.LAST_LIVE == self.LIVEMODE) - and (self.LAST_RECD_INFO == self.RECD_INFO_ON) - and (self.LAST_TRANSCODING == self.TRANSCODING) - and (self.LAST_MESHING == self.MESHING) - ): - return - - #something's changing so start counting anew - self.resetWidgetFadeTimer() - - pos = [] - if (self.RECD_INFO_ON and not self.TRANSCODING): - if (self.ca.m.MODE == Constants.MODE_PHOTO): - pos.append({"position":"pgd", "window":self.pipBgdWindow} ) - pos.append({"position":"pip", "window":self.liveVideoWindow} ) - pos.append({"position":"inb", "window":self.livePhotoWindow} ) - pos.append({"position":"inf", "window":self.infWindow} ) - elif (self.ca.m.MODE == Constants.MODE_VIDEO): - pos.append({"position":"pgd", "window":self.pipBgdWindow} ) - pos.append({"position":"pip", "window":self.slowLiveVideoWindow} ) - pos.append({"position":"inb", "window":self.playOggWindow} ) - pos.append({"position":"inf", "window":self.infWindow} ) - elif (self.ca.m.MODE == Constants.MODE_AUDIO): - pos.append({"position":"pgd", "window":self.pipBgdWindow} ) - pos.append({"position":"pip", "window":self.liveVideoWindow} ) - pos.append({"position":"inb", "window":self.livePhotoWindow} ) - pos.append({"position":"inf", "window":self.infWindow} ) - elif (not self.RECD_INFO_ON and not self.TRANSCODING): - if (self.ca.m.MODE == Constants.MODE_PHOTO): - if (self.LIVEMODE): - pos.append({"position":"img", "window":self.liveVideoWindow} ) - pos.append({"position":"max", "window":self.maxWindow} ) - pos.append({"position":"eye", "window":self.recordWindow} ) - else: - pos.append({"position":"img", "window":self.livePhotoWindow} ) - pos.append({"position":"pgd", "window":self.pipBgdWindow} ) - pos.append({"position":"pip", "window":self.liveVideoWindow} ) - if (not self.MESHING): - pos.append({"position":"max", "window":self.maxWindow} ) - pos.append({"position":"inf", "window":self.infWindow} ) - else: - pos.append({"position":"tmr", "window":self.progressWindow} ) - elif (self.ca.m.MODE == Constants.MODE_VIDEO): - if (self.LIVEMODE): - pos.append({"position":"img", "window":self.liveVideoWindow} ) - pos.append({"position":"max", "window":self.maxWindow} ) - pos.append({"position":"eye", "window":self.recordWindow} ) - pos.append({"position":"prg", "window":self.progressWindow} ) - else: - pos.append({"position":"img", "window":self.playOggWindow} ) - pos.append({"position":"pgd", "window":self.pipBgdWindow} ) - pos.append({"position":"pip", "window":self.slowLiveVideoWindow} ) - if (not self.MESHING): - pos.append({"position":"max", "window":self.maxWindow} ) - pos.append({"position":"scr", "window":self.scrubWindow} ) - pos.append({"position":"inf", "window":self.infWindow} ) - else: - pos.append({"position":"tmr", "window":self.progressWindow} ) - elif (self.ca.m.MODE == Constants.MODE_AUDIO): - if (self.LIVEMODE): - pos.append({"position":"img", "window":self.liveVideoWindow} ) - pos.append({"position":"eye", "window":self.recordWindow} ) - pos.append({"position":"prg", "window":self.progressWindow} ) - else: - pos.append({"position":"img", "window":self.livePhotoWindow} ) - pos.append({"position":"pgd", "window":self.pipBgdWindow} ) - pos.append({"position":"pip", "window":self.liveVideoWindow} ) - if (not self.MESHING): - pos.append({"position":"scr", "window":self.scrubWindow} ) - pos.append({"position":"inf", "window":self.infWindow} ) - else: - pos.append({"position":"tmr", "window":self.progressWindow} ) - elif (self.TRANSCODING): - pos.append({"position":"tmr", "window":self.progressWindow} ) - - for i in range (0, len(self.windowStack)): - self.windowStack[i].hide_all() - - self.hideAllWindows() - self.updatePos( pos ) - - for i in range (0, len(self.windowStack)): - self.windowStack[i].show_all() - - self.LAST_MODE = self.ca.m.MODE - self.LAST_FULLSCREEN = self.FULLSCREEN - self.LAST_LIVE = self.LIVEMODE - self.LAST_RECD_INFO = self.RECD_INFO_ON - self.LAST_TRANSCODING = self.TRANSCODING - self.LAST_MESHING = self.MESHING - - - def debugWindows( self ): - for i in range (0, len(self.windowStack)): - print self.windowStack[i], self.windowStack[i].get_size(), self.windowStack[i].get_position() - - - def showWidgets( self ): - pos = [] - if (self.ca.m.MODE == Constants.MODE_PHOTO): - if (not self.LIVEMODE): - pos.append({"position":"pgd", "window":self.pipBgdWindow} ) - pos.append({"position":"pip", "window":self.liveVideoWindow} ) - if (not self.MESHING): - pos.append({"position":"max", "window":self.maxWindow} ) - pos.append({"position":"inf", "window":self.infWindow} ) - else: - pos.append({"position":"max", "window":self.maxWindow} ) - pos.append({"position":"eye", "window":self.recordWindow} ) - elif (self.ca.m.MODE == Constants.MODE_VIDEO): - if (not self.LIVEMODE): - pos.append({"position":"pgd", "window":self.pipBgdWindow} ) - pos.append({"position":"pip", "window":self.slowLiveVideoWindow} ) - if (not self.MESHING): - pos.append({"position":"max", "window":self.maxWindow} ) - pos.append({"position":"scr", "window":self.scrubWindow} ) - pos.append({"position":"inf", "window":self.infWindow} ) - else: - pos.append({"position":"max", "window":self.maxWindow} ) - pos.append({"position":"eye", "window":self.recordWindow} ) - pos.append({"position":"prg", "window":self.progressWindow} ) - elif (self.ca.m.MODE == Constants.MODE_AUDIO): - if (not self.LIVEMODE): - pos.append({"position":"pgd", "window":self.pipBgdWindow} ) - pos.append({"position":"pip", "window":self.liveVideoWindow} ) - if (not self.MESHING): - pos.append({"position":"scr", "window":self.scrubWindow} ) - pos.append({"position":"inf", "window":self.infWindow} ) - else: - pos.append({"position":"eye", "window":self.recordWindow} ) - pos.append({"position":"prg", "window":self.progressWindow} ) - - self.updatePos( pos ) - - - def updatePos( self, pos ): - #now move those pieces where they need to be... - for i in range (0, len(self.windowStack)): - for j in range (0, len(pos)): - if (self.windowStack[i] == pos[j]["window"]): - if (pos[j]["position"] == "img"): - self.setImgLocDim( pos[j]["window"] ) - elif (pos[j]["position"] == "max"): - self.setMaxLocDim( pos[j]["window"] ) - elif (pos[j]["position"] == "pip"): - self.setPipLocDim( pos[j]["window"] ) - elif (pos[j]["position"] == "pgd"): - self.setPipBgdLocDim( pos[j]["window"] ) - elif (pos[j]["position"] == "eye"): - self.setEyeLocDim( pos[j]["window"] ) - elif (pos[j]["position"] == "inb"): - self.setInbLocDim( pos[j]["window"]) - elif (pos[j]["position"] == "prg"): - self.setPrgLocDim( pos[j]["window"]) - elif (pos[j]["position"] == "tmr"): - self.setTmrLocDim( pos[j]["window"]) - elif (pos[j]["position"] == "scr"): - self.setScrLocDim( pos[j]["window"]) - elif (pos[j]["position"] == "inf"): - self.setInfLocDim( pos[j]["window"]) - - - def removeThumb( self, recd ): - kids = self.thumbTray.get_children() - for i in range (0, len(kids)): - if (kids[i].recd == recd): - self.thumbTray.remove_item(kids[i]) - kids[i].cleanUp() - kids[i].disconnect( kids[i].getButtClickedId() ) - kids[i].setButtClickedId(0) - - - def addThumb( self, recd, forceScroll ): - butt = RecdButton( self, recd ) - BUTT_CLICKED_ID = butt.connect( "clicked", self._thumbClicked, recd ) - butt.setButtClickedId(BUTT_CLICKED_ID) - self.thumbTray.add_item( butt, len(self.thumbTray.get_children()) ) - butt.show() - if (forceScroll): - self.thumbTray.scroll_to_end() - - - def removeThumbs( self ): - kids = self.thumbTray.get_children() - for i in range (0, len(kids)): - self.thumbTray.remove_item(kids[i]) - kids[i].cleanUp() - if (kids[i].getButtClickedId() != 0): - kids[i].disconnect( kids[i].getButtClickedId() ) - - - def _thumbClicked( self, button, recd ): - self.showThumbSelection( recd ) - - - def infoButtonClicked( self ): - self.RECD_INFO_ON = not self.RECD_INFO_ON - - centerKid = self.centerBox.get_child() - if (centerKid != None): - self.centerBox.remove( centerKid ) - - bottomKid = self.bottomCenter.get_child() - if (bottomKid != None): - self.bottomCenter.remove( bottomKid ) - - if (not self.RECD_INFO_ON): - if (self.ca.m.MODE == Constants.MODE_PHOTO): - self.bottomCenter.add( self.namePanel ) - self.bottomCenter.show_all( ) - else: - self.centerBox.add( self.infoBox ) - self.centerBox.show_all( ) - self.bottomCenter.add( self.namePanel ) - self.bottomCenter.show_all( ) - - self.updateVideoComponents( ) - - - def showMeshRecd( self, recd ): - record.Record.log.debug('showMeshRecd: heres the downloaded recd to display...') - - #if this thumbnail is being shown, add the option to copy it now - kids = self.thumbTray.get_children() - for i in range (0, len(kids)): - if (kids[i].recd == recd): - kids[i].addCopyMenuItem() - - if (recd == self.shownRecd): - record.Record.log.debug('showMeshRecd: and since were still looking at same recd, here it is!') - self.showThumbSelection( recd ) - - - def updateMeshProgress( self, progressMade, recd ): - self.resetWidgetFadeTimer() - if (self.shownRecd != recd): - if (self.shownRecd == None): - type = Constants.mediaTypes[recd.type][Constants.keyIstr] - if (progressMade): - msg = Constants.istrDownloadingFrom% {"1":type, "2":recd.meshDownloadingFromNick} - self.progressWindow.updateProgress(recd.meshDownlodingPercent, msg) - - else: - type = Constants.mediaTypes[recd.type][Constants.keyIstr] - if (progressMade): - msg = Constants.istrDownloadingFrom% {"1":type, "2":recd.meshDownloadingFromNick} - self.progressWindow.updateProgress(recd.meshDownlodingPercent, msg) - - else: - type = Constants.mediaTypes[recd.type][Constants.keyIstr] - msg = Constants.istrCannotDownload % {"1":type} - self.progressWindow.updateProgress(0, msg) - - - def showThumbSelection( self, recd ): - lastRecd = self.shownRecd - self.shownRecd = recd - - #do we need to know the type, since we're showing based on the mode of the app? - if (recd.type == Constants.TYPE_PHOTO): - self.showPhoto( recd ) - elif (recd.type == Constants.TYPE_VIDEO): - self.showVideo( recd ) - elif (recd.type == Constants.TYPE_AUDIO): - self.showAudio( recd ) - - if (self.shownRecd != lastRecd): - self.photoXoPanel.updateXoColors(self.shownRecd.colorStroke.hex, self.shownRecd.colorFill.hex) - - bottomKid = self.bottomCenter.get_child() - if (bottomKid != None): - self.bottomCenter.remove( bottomKid ) - - if (recd.type == Constants.TYPE_PHOTO): - self.bottomCenter.add( self.namePanel ) - elif (recd.type == Constants.TYPE_VIDEO or recd.type == Constants.TYPE_AUDIO): - if (self.RECD_INFO_ON): - self.bottomCenter.add( self.namePanel ) - self.bottomCenter.show_all() - - self.resetWidgetFadeTimer() - - - def showAudio( self, recd ): - self.LIVEMODE = False - - #if (recd != self.shownRecd): - pixbuf = recd.getAudioImagePixbuf() - img = hippo.cairo_surface_from_gdk_pixbuf(pixbuf) - self.livePhotoCanvas.setImage( img ) - #self.shownRecd = recd - self.showRecdMeta(recd) - - downloading = self.ca.requestMeshDownload(recd) - self.MESHING = downloading - record.Record.log.debug("showAudio: downloading->" + str(downloading)) - if (not downloading): - self.progressWindow.updateProgress(0, "") - mediaFilepath = recd.getMediaFilepath( ) - record.Record.log.debug("showAudio: mediaFilepath->" + str(mediaFilepath)) - if (mediaFilepath != None): - videoUrl = "file://" + str( mediaFilepath ) - self.ca.gplay.setLocation(videoUrl) - self.scrubWindow.doPlay() - - self.updateVideoComponents() - - - def showVideo( self, recd ): - logger.debug('showVideo') - - downloading = self.ca.requestMeshDownload(recd) - - if (not downloading): - self.progressWindow.updateProgress(0, "") - - self.MESHING = downloading - self.LIVEMODE = False - #self.shownRecd = recd - self.updateVideoComponents() - gobject.idle_add( self.showVideo2, recd, downloading ) - - - def showVideo2( self, recd, downloading ): - self.showRecdMeta(recd) - - ableToShowVideo = False - if (not downloading): - mediaFilepath = recd.getMediaFilepath() - if (mediaFilepath != None): - logger.debug('showVideo2 file=%s' % mediaFilepath) - self.ca.glive.stop() - self.ca.glivex.play() - videoUrl = "file://" + str( mediaFilepath ) - self.ca.gplay.setLocation(videoUrl) - self.scrubWindow.doPlay() - ableToShowVideo = True - - if (not ableToShowVideo): - # FIXME is this correct? - self.ca.glive.stop() - self.ca.glivex.play() - thumbFilepath = recd.getThumbFilepath( ) - logger.debug('showVideo3 file=%s' % thumbFilepath) - thumbUrl = "file://" + str( thumbFilepath ) - self.ca.gplay.setLocation(thumbUrl) - - - def deleteThumbSelection( self, recd ): - self.ca.m.deleteRecorded( recd ) - self.ca.glive.play() - self.removeThumb( recd ) - self.removeIfSelectedRecorded( recd ) - - - def removeIfSelectedRecorded( self, recd ): - if (recd == self.shownRecd): - if (recd.type == Constants.TYPE_PHOTO): - self.livePhotoCanvas.setImage( None ) - elif (recd.type == Constants.TYPE_VIDEO): - self.ca.gplay.stop() - self.startLiveVideo() - elif (recd.type == Constants.TYPE_AUDIO): - self.livePhotoCanvas.setImage( None ) - self.startLiveAudio() - - self.RECD_INFO_ON = False - self.LIVEMODE = True - self.updateVideoComponents() - - self.showLiveVideoTags() - - - def startLiveAudio( self ): - self.ca.m.setUpdating(True) - self.ca.gplay.stop() - - self.showLiveVideoTags() - self.LIVEMODE = True - self.updateVideoComponents() - self.ca.m.setUpdating(False) - - - def showPostProcessGfx( self, show ): - #not self.FULLSCREEN - centerKid = self.centerBox.get_child() - if (centerKid != None): - self.centerBox.remove( centerKid ) - - if ( show ): - self.centerBox.add( self.backgdCanvasBox ) - self.centerBox.show_all() - else: - self.backgdCanvas.setImage( None ) - - - def setPostProcessPixBuf( self, pixbuf ): - if (pixbuf.get_width()>self.__class__.dim_THUMB_WIDTH): - pixbuf = pixbuf.scale_simple(self.__class__.dim_THUMB_WIDTH, self.__class__.dim_THUMB_HEIGHT, gtk.gdk.INTERP_NEAREST) - - pixbuf = utils.grayScalePixBuf(pixbuf, True) - img = hippo.cairo_surface_from_gdk_pixbuf(pixbuf) - self.backgdCanvas.setImage(img) - - - def showRecdMeta( self, recd ): - self.photographerNameLabel.set_label( recd.recorderName ) - self.nameTextfield.set_text( recd.title ) - self.nameTextfield.set_sensitive( True ) - self.tagsBuffer.set_text( recd.tags ) - self.dateDateLabel.set_label( utils.getDateString(recd.time) ) - - self.photographerPanel.show() - self.namePanel.show() - self.datePanel.show() - self.tagsPanel.show() - self.tagsField.set_sensitive(True) - - - def setWaitCursor( self, win ): - win.set_cursor( gtk.gdk.Cursor(gtk.gdk.WATCH) ) - - - def setDefaultCursor( self, win ): - win.set_cursor( None ) - - -class PhotoCanvas(P5): - def __init__(self): - P5.__init__(self) - self.img = None - self.drawImg = None - self.SCALING_IMG_ID = 0 - self.cacheWid = -1 - self.modify_bg( gtk.STATE_NORMAL, Constants.colorBlack.gColor ) - self.modify_bg( gtk.STATE_INSENSITIVE, Constants.colorBlack.gColor ) - - - def draw(self, ctx, w, h): - self.background( ctx, Constants.colorBlack, w, h ) - - if (self.img != None): - - if (w == self.img.get_width()): - self.cacheWid == w - self.drawImg = self.img - - #only scale images when you need to, otherwise you're wasting cycles, fool! - if (self.cacheWid != w): - if (self.SCALING_IMG_ID == 0): - self.drawImg = None - self.SCALING_IMG_ID = gobject.idle_add( self.resizeImage, w, h ) - - if (self.drawImg != None): - #center the image based on the image size, and w & h - ctx.set_source_surface(self.drawImg, (w/2)-(self.drawImg.get_width()/2), (h/2)-(self.drawImg.get_height()/2)) - ctx.paint() - - self.cacheWid = w - - - def setImage(self, img): - self.cacheWid = -1 - self.img = img - self.drawImg = None - self.queue_draw() - - - def resizeImage(self, w, h): - self.SCALING_IMG_ID = 0 - if (self.img == None): - return - - #use image size in case 640 no more - scaleImg = cairo.ImageSurface(cairo.FORMAT_ARGB32, w, h) - sCtx = cairo.Context(scaleImg) - sScl = (w+0.0)/(self.img.get_width()+0.0) - sCtx.scale( sScl, sScl ) - sCtx.set_source_surface( self.img, 0, 0 ) - sCtx.paint() - self.drawImg = scaleImg - self.cacheWid = w - self.queue_draw() - - -class xoPanel(P5): - def __init__(self): - P5.__init__(self) - self.xoGuy = None - self.lastStroke = None - self.lastFill = None - - - def updateXoColors( self, strokeHex, fillHex ): - if (self.lastStroke != None): - if ((self.lastStroke == strokeHex) and (self.lastFill == fillHex)): - return - - lastStroke = strokeHex - lastFill = fillHex - self.xoGuy = utils.loadSvg(Constants.xoGuySvgData, strokeHex, fillHex) - self.queue_draw() - - - def draw(self, ctx, w, h): - #todo: 2x buffer - self.background( ctx, Constants.colorButton, w, h ) - - if (self.xoGuy != None): - #todo: scale mr xo to fit in his box - ctx.scale( .6, .6 ) - self.xoGuy.render_cairo( ctx ) - - -class ScrubberWindow(gtk.Window): - def __init__(self, ui): - gtk.Window.__init__(self) - self.ui = ui - self.UPDATE_INTERVAL = 500 - self.UPDATE_SCALE_ID = 0 - self.CHANGED_ID = 0 - self.was_playing = False - self.p_position = gst.CLOCK_TIME_NONE - self.p_duration = gst.CLOCK_TIME_NONE - - self.hbox = gtk.HBox() - self.hbox.modify_bg( gtk.STATE_NORMAL, Constants.colorBlack.gColor ) - self.hbox.modify_bg( gtk.STATE_INSENSITIVE, Constants.colorBlack.gColor ) - self.add( self.hbox ) - - self.button = gtk.Button() - buttBox = gtk.EventBox() - buttBox.add(self.button) - buttBox.modify_bg( gtk.STATE_NORMAL, Constants.colorBlack.gColor ) - self.button.set_image( Constants.recPlayImg ) - self.button.set_property('can-default', True) - self.button.set_relief(gtk.RELIEF_NONE) - self.button.set_size_request( self.ui.recordButtWd, self.ui.recordButtWd ) - buttBox.set_size_request( self.ui.recordButtWd, self.ui.recordButtWd ) - #self.button.set_border_width( UI.dim_INSET/2 ) - self.button.show() - - buttBox.modify_bg( gtk.STATE_NORMAL, Constants.colorBlack.gColor ) - self.button.modify_bg( gtk.STATE_ACTIVE, Constants.colorBlack.gColor ) - - self.button.connect('clicked', self._buttonClickedCb) - self.hbox.pack_start(buttBox, expand=False) - - self.adjustment = gtk.Adjustment(0.0, 0.00, 100.0, 0.1, 1.0, 1.0) - self.hscale = gtk.HScale(self.adjustment) - self.hscale.set_draw_value(False) - self.hscale.set_update_policy(gtk.UPDATE_CONTINUOUS) - hscaleBox = gtk.EventBox() - hscaleBox.modify_bg( gtk.STATE_NORMAL, Constants.colorBlack.gColor ) - hscaleBox.add( self.hscale ) - self.hscale.connect('button-press-event', self._scaleButtonPressCb) - self.hscale.connect('button-release-event', self._scaleButtonReleaseCb) - self.hbox.pack_start(hscaleBox, expand=True) - - - def removeCallbacks( self ): - if (self.UPDATE_SCALE_ID != 0): - gobject.source_remove(self.UPDATE_SCALE_ID) - self.UPDATE_SCALE_ID = 0 - if (self.CHANGED_ID != 0): - gobject.source_remove(self.CHANGED_ID) - self.CHANGED_ID = 0 - - - def reset(self): - self.adjustment.set_value(0) - - - def _buttonClickedCb(self, widget): - self.play_toggled() - - - def set_button_play(self): - self.button.set_image(Constants.recPlayImg) - - - def set_button_pause(self): - self.button.set_image(Constants.recPauseImg) - - - def play_toggled(self): - self.p_position, self.p_duration = self.ui.ca.gplay.queryPosition() - if (self.p_position == self.p_duration): - self.ui.ca.gplay.seek(0) - self.ui.ca.gplay.pause() - - if self.ui.ca.gplay.is_playing(): - self.ui.ca.gplay.pause() - self.set_button_play() - else: - #if self.ui.ca.gplay.error: - # #todo: check if we have "error", and also to disable everything - # self.button.set_disabled() - #else: - self.doPlay() - - - def doPlay(self): - self.ui.ca.gplay.play() - if self.UPDATE_SCALE_ID == 0: - self.UPDATE_SCALE_ID = gobject.timeout_add(self.UPDATE_INTERVAL, self._updateScaleCb) - self.set_button_pause() - - - def _scaleButtonPressCb(self, widget, event): - #self.button.set_sensitive(False) - self.was_playing = self.ui.ca.gplay.is_playing() - if self.was_playing: - self.ui.ca.gplay.pause() - - # don't timeout-update position during seek - if self.UPDATE_SCALE_ID != 0: - gobject.source_remove(self.UPDATE_SCALE_ID) - self.UPDATE_SCALE_ID = 0 - - # make sure we get changed notifies - if self.CHANGED_ID == 0: - self.CHANGED_ID = self.hscale.connect('value-changed', self._scaleValueChangedCb) - - - def _scaleButtonReleaseCb(self, widget, event): - # see seek.cstop_seek - widget.disconnect(self.CHANGED_ID) - self.CHANGED_ID = 0 - - #self.button.set_sensitive(True) - if self.was_playing: - self.ui.ca.gplay.play() - - if self.UPDATE_SCALE_ID != 0: - pass - #print('Had a previous update timeout id') - else: - self.UPDATE_SCALE_ID = gobject.timeout_add(self.UPDATE_INTERVAL, self._updateScaleCb) - - - def _scaleValueChangedCb(self, scale): - real = long(scale.get_value() * self.p_duration / 100) # in ns - self.ui.ca.gplay.seek(real) - # allow for a preroll - self.ui.ca.gplay.get_state(timeout=50*gst.MSECOND) # 50 ms - - - def _updateScaleCb(self): - self.p_position, self.p_duration = self.ui.ca.gplay.queryPosition() - if self.p_position != gst.CLOCK_TIME_NONE: - value = self.p_position * 100.0 / self.p_duration - if (value > 99): - value = 99 - elif (value < 0): - value = 0 - - self.adjustment.set_value(value) - - if self.ui.ca.gplay.is_playing() and (self.p_position == self.p_duration): - self.ui.ca.gplay.pause() - self.set_button_play() - - return True - - -class MaxButton(P5Button): - def __init__(self, ui): - P5Button.__init__(self) - self.ui = ui - - self.modify_bg( gtk.STATE_NORMAL, Constants.colorBlack.gColor ) - self.modify_bg( gtk.STATE_INSENSITIVE, Constants.colorBlack.gColor ) - - xs = [] - ys = [] - xs.append(0) - ys.append(0) - xs.append(self.ui.maxw) - ys.append(0) - xs.append(self.ui.maxw) - ys.append(self.ui.maxh) - xs.append(0) - ys.append(self.ui.maxh) - poly = Polygon( xs, ys ) - butt = Button( poly, 0, 0) - butt.addActionListener( self ) - self.maxS = "max" - butt.setActionCommand( self.maxS ) - self._butts.append( butt ) - - - def draw(self, ctx, w, h): - if (self.ui.FULLSCREEN): - Constants.maxEnlargeSvg.render_cairo( ctx ) - else: - Constants.maxReduceSvg.render_cairo( ctx ) - - - def fireButton(self, actionCommand): - if (actionCommand == self.maxS): - self.ui.doFullscreen() - - -class InfButton(P5Button): - #todo: just a gtk.Image here, no? - def __init__(self, ui): - P5Button.__init__(self) - self.ui = ui - - self.modify_bg( gtk.STATE_NORMAL, Constants.colorBlack.gColor ) - self.modify_bg( gtk.STATE_INSENSITIVE, Constants.colorBlack.gColor ) - - self.set_size_request( 75, 75 ) - - xs = [] - ys = [] - xs.append(0) - ys.append(0) - xs.append(75) - ys.append(0) - xs.append(75) - ys.append(75) - xs.append(0) - ys.append(75) - poly = Polygon( xs, ys ) - butt = Button( poly, 0, 0) - butt.addActionListener( self ) - self.infS = "inf" - butt.setActionCommand( self.infS ) - self._butts.append( butt ) - - - def draw(self, ctx, w, h): - self.background( ctx, Constants.colorBlack, w, h ) - Constants.infoOnSvg.render_cairo( ctx ) - - - def fireButton(self, actionCommand): - if (actionCommand == self.infS): - self.ui.infoButtonClicked() - - -class RecordButton(gtk.Button): - def __init__(self): - gtk.Button.__init__(self) - self.sens = True - self.img = None - #todo: check on record state, compare button imgs - - - def set_sensitive(self, sen, full): - if (sen == self.sens): - return - self.sens = sen - - if (self.sens): - self.set_image( Constants.recImg ) - else: - if (full): - self.set_image( Constants.fullInsensitiveImg ) - else: - self.set_image( Constants.recInsensitiveImg ) - - super(RecordButton, self).set_sensitive(self.sens) - - - def doRecordButton(self): - if (not self.sens): - return - self.set_image( Constants.recRedImg ) - - - def doNormalButton(self): - if (not self.sens): - return - self.set_image( Constants.recImg ) - - -class RecordWindow(gtk.Window): - def __init__(self, ui): - gtk.Window.__init__(self) - self.ui = ui - self.num = -1 - - self.modify_bg( gtk.STATE_NORMAL, Constants.colorBlack.gColor ) - - self.shutterButton = RecordButton() - self.shutterButton.set_size_request(self.ui.recordButtWd, self.ui.recordButtWd) - self.shutterButton.set_relief(gtk.RELIEF_NONE) - self.shutterButton.set_image( Constants.recImg ) - self.shutterButton.connect("clicked", self.ui._shutterClickCb) - - shutterBox = gtk.EventBox() - shutterBox.add( self.shutterButton ) - shutterBox.set_size_request( self.ui.controlBarHt, self.ui.controlBarHt ) - - shutterBox.modify_bg( gtk.STATE_NORMAL, Constants.colorBlack.gColor ) - self.shutterButton.modify_bg( gtk.STATE_ACTIVE, Constants.colorBlack.gColor ) - - hbox = gtk.HBox() - self.add( hbox ) - leftPanel = gtk.VBox() - self.leftEvent = gtk.EventBox() - self.leftEvent.modify_bg( gtk.STATE_NORMAL, Constants.colorBlack.gColor ) - self.leftEvent.add( leftPanel ) - self.leftEvent.set_size_request(self.ui.vw/2-self.ui.controlBarHt, -1) - hbox.pack_start( self.leftEvent, expand=True ) - - hbox.pack_start( shutterBox, expand=False ) - - rightPanel = gtk.VBox() - self.rightEvent = gtk.EventBox() - self.rightEvent.modify_bg( gtk.STATE_NORMAL, Constants.colorBlack.gColor ) - self.rightEvent.add( rightPanel ) - hbox.pack_start( self.rightEvent, expand=True ) - - self.rightPanelLabel = gtk.Label() - rightPanel.pack_start( self.rightPanelLabel ) - - - def updateCountdown(self, num): - if(num>0): - if (num != self.num): - ok = self.getCairoCountdown(num) - self.shutterButton.set_image(ok) - self.num = num - else: - self.num = -1 - - - def getCairoCountdown(self, num): - return Constants.countdownImgs[int(num)] - - - def minimize( self ): - self.leftEvent.set_size_request(-1, -1) - self.rightEvent.set_size_request(-1, -1) - - - def maximize( self ): - w = self.ui.vw/2-self.ui.controlBarHt - self.rightEvent.set_size_request(w, -1) - self.leftEvent.set_size_request(w, -1) - - - def displayDiscFullText( self, full ): - if (not full or self.ui.ca.m.MODE != Constants.MODE_PHOTO): - self.rightPanelLabel.set_text("") - self.minimize() - else: - fullMessage = Constants.istrYourDiskIsFull % {"1":Constants.istrJournal} - self.rightPanelLabel.set_text("<b><span foreground='gray'>" + fullMessage + "</span></b>") - self.rightPanelLabel.set_use_markup( True ) - self.rightPanelLabel.set_alignment(1, 1) - self.maximize() - - -class ProgressWindow(gtk.Window): - def __init__(self, ui): - gtk.Window.__init__(self) - self.ui = ui - self.update = "" - - self.modify_bg( gtk.STATE_NORMAL, Constants.colorBlack.gColor ) - self.modify_bg( gtk.STATE_INSENSITIVE, Constants.colorBlack.gColor ) - - eb = gtk.EventBox() - eb.modify_bg( gtk.STATE_NORMAL, Constants.colorBlack.gColor ) - eb.modify_bg( gtk.STATE_INSENSITIVE, Constants.colorBlack.gColor ) - self.add( eb ) - - vb = gtk.VBox() - vb.set_border_width(5) #todo: use variable - eb.add(vb) - - self.progBar = gtk.ProgressBar() - self.progBar.modify_bg( gtk.STATE_NORMAL, Constants.colorBlack.gColor ) - self.progBar.modify_bg( gtk.STATE_ACTIVE, Constants.colorBlack.gColor ) - self.progBar.modify_bg( gtk.STATE_PRELIGHT, Constants.colorBlack.gColor ) - self.progBar.modify_bg( gtk.STATE_SELECTED, Constants.colorBlack.gColor ) - self.progBar.modify_bg( gtk.STATE_INSENSITIVE, Constants.colorBlack.gColor ) - vb.add( self.progBar ) - - hbox = gtk.HBox() - vb.add( hbox ) - self.infoLabel = gtk.Label() - self.infoLabel.set_alignment( 1, .5 ) - self.infoLabel.set_text( "<b><span foreground='black'>SPACE</span></b>") - self.infoLabel.set_use_markup( True ) - hbox.pack_start(self.infoLabel) - - - def updateProgress( self, amt, update, color='white' ): - logging.debug('updateProgress %s' % amt) - - self.progBar.set_fraction( amt ) - if (update != None and update != self.update): - self.update = update - self.infoLabel.set_text( "<b><span foreground='" + color + "'>"+self.update+"</span></b>") - self.infoLabel.set_use_markup( True ) - - if (self.update==""): - self.infoLabel.set_text( "<b><span foreground='black'>SPACE</span></b>") - self.infoLabel.set_use_markup( True ) - - if (amt >= 1): - self.progBar.set_fraction( 0 ) - - -class PhotoToolbar(gtk.Toolbar): - def __init__(self): - gtk.Toolbar.__init__(self) - - img = ToolButton('media-photo') - img.connect('clicked', self._shutterClickCb) - img.get_icon_widget().set_property( 'fill-color', Instance.colorFill.hex ) - img.get_icon_widget().set_property( 'stroke-color', Instance.colorStroke.hex ) - self.insert(img, -1) - img.set_sensitive(False) - img.show() - - separator = gtk.SeparatorToolItem() - separator.set_draw(False) - separator.set_expand(True) - self.insert(separator, -1) - separator.show() - - timerCbb = gtk.combo_box_new_text() - self.timerCb = ToolComboBox(combo=timerCbb, label_text=Constants.istrTimer) - for i in range (0, len(Constants.TIMERS)): - if (i == 0): - self.timerCb.combo.append_text( Constants.istrNow ) - else: - self.timerCb.combo.append_text(istrSeconds(Constants.TIMERS[i])) - self.timerCb.combo.set_active(0) - self.insert( self.timerCb, -1 ) - - - def _shutterClickCb(self, button): - pass - #self.ui.doShutter() - - - def getTimer(self): - return Constants.TIMERS[self.timerCb.combo.get_active()] - - -class VideoToolbar(gtk.Toolbar): - def __init__(self): - gtk.Toolbar.__init__(self) - - img = ToolButton('media-video') - img.connect('clicked', self._shutterClickCb) - img.get_icon_widget().set_property( 'fill-color', Instance.colorFill.hex ) - img.get_icon_widget().set_property( 'stroke-color', Instance.colorStroke.hex ) - self.insert(img, -1) - img.set_sensitive(False) - img.show() - - separator = gtk.SeparatorToolItem() - separator.set_draw(False) - separator.set_expand(True) - self.insert(separator, -1) - separator.show() - - combo = gtk.combo_box_new_text() - self.quality = ToolComboBox(combo=combo, - label_text=Constants.istrQuality+':') - self.quality.combo.append_text(Constants.istrLowQuality) - 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(Constants.istrHighQuality) - self.quality.combo.set_active(0) - self.insert(self.quality, -1 ) - - timerCbb = gtk.combo_box_new_text() - self.timerCb = ToolComboBox(combo=timerCbb, label_text=Constants.istrTimer) - for i in range (0, len(Constants.TIMERS)): - if (i == 0): - self.timerCb.combo.append_text( Constants.istrNow ) - else: - self.timerCb.combo.append_text(istrSeconds(Constants.TIMERS[i])) - self.timerCb.combo.set_active(0) - self.insert( self.timerCb, -1 ) - - separator2 = gtk.SeparatorToolItem() - separator2.set_draw(False) - separator2.set_expand(False) - separator2.set_size_request(UI.dim_INSET, -1) - self.insert( separator2, -1 ) - - durCbb = gtk.combo_box_new_text() - self.durCb = ToolComboBox(combo=durCbb, label_text=Constants.istrDuration) - for i in range (0, len(Constants.DURATIONS)): - self.durCb.combo.append_text(istrMinutes(Constants.DURATIONS[i])) - self.durCb.combo.set_active(0) - self.insert(self.durCb, -1 ) - - - def _shutterClickCb(self, button): - pass - #self.ui.doShutter() - - - def getTimer(self): - return Constants.TIMERS[self.timerCb.combo.get_active()] - - - def getDuration(self): - return 60 * Constants.DURATIONS[self.durCb.combo.get_active()] - - - def getQuality(self): - return self.quality.combo.get_active() - -class AudioToolbar(gtk.Toolbar): - def __init__(self): - gtk.Toolbar.__init__(self) - - img = ToolButton('media-audio') - img.connect('clicked', self._shutterClickCb) - img.get_icon_widget().set_property( 'fill-color', Instance.colorFill.hex ) - img.get_icon_widget().set_property( 'stroke-color', Instance.colorStroke.hex ) - self.insert(img, -1) - img.set_sensitive(False) - img.show() - - separator = gtk.SeparatorToolItem() - separator.set_draw(False) - separator.set_expand(True) - self.insert(separator, -1) - separator.show() - - timerCbb = gtk.combo_box_new_text() - self.timerCb = ToolComboBox(combo=timerCbb, label_text=Constants.istrTimer) - for i in range (0, len(Constants.TIMERS)): - if (i == 0): - self.timerCb.combo.append_text( Constants.istrNow ) - else: - self.timerCb.combo.append_text(istrSeconds(Constants.TIMERS[i])) - self.timerCb.combo.set_active(0) - self.insert( self.timerCb, -1 ) - - separator2 = gtk.SeparatorToolItem() - separator2.set_draw(False) - separator2.set_expand(False) - separator2.set_size_request(UI.dim_INSET, -1) - self.insert( separator2, -1 ) - - durCbb = gtk.combo_box_new_text() - self.durCb = ToolComboBox(combo=durCbb, label_text=Constants.istrDuration) - for i in range (0, len(Constants.DURATIONS)): - self.durCb.combo.append_text(istrMinutes(Constants.DURATIONS[i])) - self.durCb.combo.set_active(0) - self.insert(self.durCb, -1 ) - - - def _shutterClickCb(self, button): - pass - #self.ui.doShutter() - - - def getTimer(self): - return Constants.TIMERS[self.timerCb.combo.get_active()] - - - def getDuration(self): - return 60 * Constants.DURATIONS[self.durCb.combo.get_active()] @@ -2,15 +2,11 @@ import base64 import rsvg import re import os -import statvfs -import cairo -import gc import gtk import time from time import strftime -import hippo -from sugar import util +import constants def getStringFromPixbuf(pixbuf): data = [""] @@ -31,9 +27,9 @@ def getPixbufFromString(str): return pbl.get_pixbuf() -def loadSvg( data, stroke, fill ): - if ((stroke == None) or (fill == None)): - return rsvg.Handle( data=data ) +def load_colored_svg(filename, stroke, fill): + path = os.path.join(constants.GFX_PATH, filename) + data = open(path, 'r').read() entity = '<!ENTITY fill_color "%s">' % fill data = re.sub('<!ENTITY fill_color .*>', entity, data) @@ -41,8 +37,7 @@ def loadSvg( data, stroke, fill ): entity = '<!ENTITY stroke_color "%s">' % stroke data = re.sub('<!ENTITY stroke_color .*>', entity, data) - return rsvg.Handle( data=data ) - + return rsvg.Handle(data=data).get_pixbuf() def getUniqueFilepath( path, i ): pathOb = os.path.abspath( path ) @@ -53,56 +48,9 @@ def getUniqueFilepath( path, i ): else: return os.path.abspath( newPath ) - -def generateThumbnail( pixbuf, scale, thumbw, thumbh ): - #need to generate thumbnail version here - thumbImg = cairo.ImageSurface(cairo.FORMAT_ARGB32, thumbw, thumbh) - tctx = cairo.Context(thumbImg) - img = hippo.cairo_surface_from_gdk_pixbuf(pixbuf) - tctx.scale(scale, scale) - tctx.set_source_surface(img, 0, 0) - tctx.paint() - gc.collect() - return thumbImg - - -def scaleSvgToDim( handle, dim ): - #todo... - scale = 1.0 - - svgDim = handle.get_dimension_data() - if (svgDim[0] > dim[0]): - pass - - return scale - +def generate_thumbnail(pixbuf): + return pixbuf.scale_simple(108, 81, gtk.gdk.INTERP_BILINEAR) def getDateString( when ): return strftime( "%c", time.localtime(when) ) - -def grayScalePixBuf2( pb, copy ): - arr = pb.get_pixels_array() - if (copy): - arr = arr.copy() - for row in arr: - for pxl in row: - y = 0.3*pxl[0][0]+0.59*pxl[1][0]+0.11*pxl[2][0] - pxl[0][0] = y - pxl[1][0] = y - pxl[2][0] = y - return gtk.gdk.pixbuf_new_from_array(arr, pb.get_colorspace(), pb.get_bits_per_sample()) - - -def grayScalePixBuf( pb, copy ): - pb2 = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, pb.get_width(), pb.get_height()) - pb.saturate_and_pixelate(pb2, 0, 0) - return pb2 - - -def getFreespaceKb( ): - stat = os.statvfs("/home") - freebytes = stat[statvfs.F_BSIZE] * stat[statvfs.F_BAVAIL] - freekb = freebytes / 1024 - return freekb - |