diff options
Diffstat (limited to 'record.py')
-rw-r--r-- | record.py | 1249 |
1 files changed, 818 insertions, 431 deletions
@@ -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 ) |