Example #1
0
class Record(activity.Activity):
    def __init__(self, handle):
        activity.Activity.__init__(self, handle)

        if Gst.version() == (1, 0, 10, 0):
            return self._incompatible()

        # for fullscreen feature, use local rather than toolkit
        self.props.enable_fullscreen_mode = False

        self._state = None
        Instance(self)

        # the main classes
        self.model = Model(self)
        self.ui_init()

        # CSCL
        self.connect("shared", self._shared_cb)
        if self.get_shared_activity():
            # have you joined or shared this activity yourself?
            if self.get_shared():
                self._joined_cb(self)
            else:
                self.connect("joined", self._joined_cb)

        # Changing to the first toolbar kicks off the rest of the setup
        if self.model.get_cameras():
            self.model.change_mode(constants.MODE_PHOTO)
        else:
            self.model.change_mode(constants.MODE_AUDIO)

        # Start live video pipeline when the video window becomes visible
        def on_defer_cb():
            self.model.set_visible(True)
            self.connect("notify::active", self.__active_cb)
            return False

        def on_event_cb(widget, event):
            if event.state == Gdk.VisibilityState.UNOBSCURED:
                GLib.timeout_add(50, on_defer_cb)
                self._media_view._video.disconnect_by_func(on_event_cb)

        self._media_view._video.add_events(
            Gdk.EventMask.VISIBILITY_NOTIFY_MASK)
        self._media_view._video.connect('visibility-notify-event', on_event_cb)

    def _incompatible(self):
        ''' Display abbreviated activity user interface with alert '''
        toolbox = ToolbarBox()
        stop = StopButton(self)
        toolbox.toolbar.add(stop)
        self.set_toolbar_box(toolbox)

        title = _('Activity not compatible with this system.')
        msg = _('Please downgrade activity and try again.')
        alert = Alert(title=title, msg=msg)
        alert.add_button(0, 'Stop', Icon(icon_name='activity-stop'))
        self.add_alert(alert)

        label = Gtk.Label(_('Uh oh, GStreamer is too old.'))
        self.set_canvas(label)

        alert.connect('response', self.__incompatible_response_cb)
        stop.connect('clicked', self.__incompatible_stop_clicked_cb,
                     alert)

        self.show_all()

    def __incompatible_stop_clicked_cb(self, button, alert):
        self.remove_alert(alert)

    def __incompatible_response_cb(self, alert, response):
        self.remove_alert(alert)
        self.close()

    def read_file(self, path):
        if hasattr(self, 'model'):
            self.model.read_file(path)

    def write_file(self, path):
        if hasattr(self, 'model'):
            self.model.write_file(path)

    def close(self, **kwargs):
        if hasattr(self, 'model'):
            self.model.close()
        activity.Activity.close(self, **kwargs)

    def __active_cb(self, widget, pspec):
        self.model.set_visible(self.props.active)

    def _shared_cb(self, activity):
        self.model.collab.set_activity_shared()

    def _joined_cb(self, activity):
        self.model.collab.joined()

    def ui_init(self):
        self._fullscreen = False
        self._showing_info = False

        # FIXME: if _thumb_tray becomes some kind of button group, we wouldn't
        # have to track which recd is active
        self._active_recd = None

        self.connect('key-press-event', self._key_pressed)

        self._active_toolbar_idx = 0

        toolbar_box = ToolbarBox()
        self._activity_toolbar_button = ActivityToolbarButton(self)
        toolbar_box.toolbar.insert(self._activity_toolbar_button, 0)
        self.set_toolbar_box(toolbar_box)
        self._toolbar = self.get_toolbar_box().toolbar

        tool_group = None
        if self.model.get_cameras():
            self._photo_button = RadioToolButton()
            self._photo_button.props.group = tool_group
            tool_group = self._photo_button
            self._photo_button.props.icon_name = 'camera-external'
            self._photo_button.props.label = _('Photo')
            self._photo_button.props.accelerator = '<ctrl>1'
            self._photo_button.props.tooltip = _(
                'Picture camera mode\n\n'
                'When the record button is pressed,\n'
                'take one picture from the camera.')
            self._photo_button.mode = constants.MODE_PHOTO
            self._photo_button.connect('clicked', self._mode_button_clicked)
            self._toolbar.insert(self._photo_button, -1)

            self._video_button = RadioToolButton()
            self._video_button.props.group = tool_group
            self._video_button.props.icon_name = 'media-video'
            self._video_button.props.accelerator = '<ctrl>2'
            self._video_button.props.label = _('Video')
            self._video_button.props.tooltip = _(
                'Video camera mode\n\n'
                'When the record button is pressed,\n'
                'take photographs many times a second,\n'
                'and record sound using the microphone,\n'
                'until the button is pressed again.')
            self._video_button.mode = constants.MODE_VIDEO
            self._video_button.connect('clicked', self._mode_button_clicked)
            self._toolbar.insert(self._video_button, -1)
        else:
            self._photo_button = None
            self._video_button = None

        self._audio_button = RadioToolButton()
        self._audio_button.props.group = tool_group
        self._audio_button.props.icon_name = 'media-audio'
        self._audio_button.props.accelerator = '<ctrl>3'
        self._audio_button.props.label = _('Audio')
        self._audio_button.props.tooltip = _(
            'Audio recording mode\n\n'
            'When the record button is pressed,\n'
            'take one photograph,\n'
            'and record sound using the microphone,\n'
            'until the button is pressed again.')
        self._audio_button.mode = constants.MODE_AUDIO
        self._audio_button.connect('clicked', self._mode_button_clicked)
        self._toolbar.insert(self._audio_button, -1)

        self._toolbar.insert(Gtk.SeparatorToolItem(), -1)

        self._mirror_btn = ToggleToolButton('mirror-horizontal')
        self._mirror_btn.set_tooltip(_(
            'Mirror view\n\n'
            'Swap left for right, as if looking at a mirror.\n'
            'Does not affect recording.'))
        self._mirror_btn.props.accelerator = '<ctrl>m'
        self._mirror_btn.show()
        self._mirror_btn.connect('toggled', self.__mirror_toggled_cb)
        self._toolbar.insert(self._mirror_btn, -1)

        self._toolbar_controls = RecordControl(self._toolbar)

        if self.model.get_cameras() and len(self.model.get_cameras()) > 1:
            switch_camera_btn = ToolButton('switch-camera')
            switch_camera_btn.set_tooltip(_('Switch camera'))
            switch_camera_btn.show()
            switch_camera_btn.connect('clicked', self.__switch_camera_click_cb)
            self._toolbar.insert(switch_camera_btn, -1)

        separator = Gtk.SeparatorToolItem()
        separator.props.draw = False
        separator.set_expand(True)
        self._toolbar.insert(separator, -1)
        self._toolbar.insert(StopButton(self), -1)
        self.get_toolbar_box().show_all()

        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('fullscreen-clicked',
                                 self._media_view_fullscreen_clicked)
        self._media_view.connect('tags-changed', self._media_view_tags_changed)
        self._media_view.show()

        self._controls_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
        trim_height_shutter_button = 7
        self._controls_hbox.set_size_request(-1, style.GRID_CELL_SIZE +
                                             trim_height_shutter_button)

        self._shutter_button = ShutterButton()
        self._shutter_button.connect("clicked", self._shutter_clicked)
        self._shutter_button.modify_bg(Gtk.StateType.NORMAL, COLOR_BLACK)
        self._controls_hbox.pack_start(self._shutter_button, True, False, 0)

        self._countdown_image = CountdownImage()
        self._controls_hbox.pack_start(self._countdown_image, True, False, 0)

        self._play_button = PlayButton()
        self._play_button.connect('clicked', self._play_pause_clicked)
        self._controls_hbox.pack_start(self._play_button, False, True, 0)

        self._playback_scale = PlaybackScale(self.model)
        self._controls_hbox.pack_start(self._playback_scale, True, True, 0)

        self._progress = ProgressInfo()
        self._controls_hbox.pack_start(self._progress, True, True, 0)

        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, False, True, 0)

        self._title_entry = Gtk.Entry()
        self._title_entry.modify_bg(Gtk.StateType.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)
        self._controls_hbox.show()

        height_tray = 150  # height of tray

        self._thumb_tray = HTray(hexpand=True, height_request=height_tray)
        self._thumb_tray.show()

        height = Gdk.Screen.height() - style.GRID_CELL_SIZE * 2 - \
            height_tray - trim_height_shutter_button
        self._media_view.set_size_request(-1, height)

        self._grid = Gtk.Grid(orientation=Gtk.Orientation.VERTICAL)
        self._media_view.props.hexpand = True
        self._media_view.props.vexpand = True
        for row in [self._media_view, self._controls_hbox, self._thumb_tray]:
            self._grid.add(row)
        self._grid.modify_bg(Gtk.StateType.NORMAL, COLOR_BLACK)
        self._grid.show()
        self.set_canvas(self._grid)

    def set_title_visible(self, visible):
        self._grid.remove(self._controls_hbox)

        if visible:
            self._grid.attach_next_to(self._controls_hbox, self._media_view,
                                      Gtk.PositionType.TOP, 1, 1)
        else:
            self._grid.attach_next_to(self._controls_hbox, self._media_view,
                                      Gtk.PositionType.BOTTOM, 1, 1)

    def __switch_camera_click_cb(self, btn):
        self.model.switch_camera()

    def __mirror_toggled_cb(self, button):
        self.model.set_mirror(button.props.active)

    def serialize(self):
        data = {}

        data['timer'] = self._toolbar_controls.get_timer_idx()
        data['duration'] = self._toolbar_controls.get_duration_idx()
        data['quality'] = self._toolbar_controls.get_quality()

        return data

    def deserialize(self, data):
        self._toolbar_controls.set_timer_idx(data.get('timer', 0))
        self._toolbar_controls.set_duration_idx(data.get('duration', 0))
        self._toolbar_controls.set_quality(data.get('quality', 0))

    def _key_pressed(self, widget, event):
        key = event.keyval
        ctrl = event.state & Gdk.ModifierType.CONTROL_MASK

        # while activity toolbar is visible, only escape key is taken
        if self._activity_toolbar_button.is_expanded():
            if key == Gdk.KEY_Escape:
                self._activity_toolbar_button.set_expanded(False)
                return True

            return False

        # while title is focused, only escape key is taken
        if self._title_entry.is_focus():
            if key == Gdk.KEY_Escape:
                self.model.set_state(constants.STATE_READY)

            return False

        # while info tags are focused, only escape key is taken
        if self._media_view.info_view.textview.is_focus():
            if key == Gdk.KEY_Escape:
                self.model.set_state(constants.STATE_READY)

            return False

        if ctrl and key == Gdk.KEY_f:
            self._toggle_fullscreen()
            return True

        if ctrl and key == Gdk.KEY_s:
            self.model.glive.stop()
            return True

        if ctrl and key == Gdk.KEY_p:
            self.model.glive.play()
            return True

        if (ctrl and key == Gdk.KEY_space) or \
           (ctrl and key == Gdk.KEY_r) or \
           key == Gdk.KEY_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)
            return True

        if key == Gdk.KEY_space and self._active_recd:
            if self._active_recd.type in (constants.TYPE_VIDEO,
                                          constants.TYPE_AUDIO):
                self.model.play_pause()
                return True

        # if viewing media, return to live mode
        if key == Gdk.KEY_Escape and self._active_recd:
            self.model.set_state(constants.STATE_READY)
            return True

        if self.model.ui_frozen():
            return True

        if ctrl and key == Gdk.KEY_c:
            self._copy_to_clipboard(self._active_recd)
            return True

        if key == Gdk.KEY_i and self._active_recd:
            self._toggle_info()
            return True

        if key == Gdk.KEY_Escape and self._fullscreen:
            self._toggle_fullscreen()
            return True

        return False

    def _play_pause_clicked(self, widget):
        self.model.play_pause()

    def set_mode(self, mode):
        self._toolbar_controls.set_mode(mode)

    # can be called from GStreamer thread, so must not do any GTK+ stuff
    def set_glive_sink(self, sink):
        return self._media_view.set_video_sink(sink)

    # can be called from GStreamer thread, so must not do any GTK+ stuff
    def set_gplay_sink(self, sink):
        return self._media_view.set_video2_sink(sink)

    def get_selected_quality(self):
        return self._toolbar_controls.get_quality()

    def get_selected_timer(self):
        return self._toolbar_controls.get_timer()

    def get_selected_duration(self):
        return self._toolbar_controls.get_duration() * 60  # convert to secs

    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()
            return

        self._shutter_button.hide()
        self._countdown_image.show()
        self._countdown_image.set_value(value)

    def _title_changed(self, widget):
        self._active_recd.setTitle(self._title_entry.get_text())

    def _media_view_media_clicked(self, widget):
        if self._play_button.props.visible and \
           self._play_button.props.sensitive:
            self._play_button.clicked()

    def _media_view_pip_clicked(self, widget):
        # clicking on the PIP always returns to live mode
        self.model.set_state(constants.STATE_READY)

    def _media_view_info_clicked(self, widget):
        self._toggle_info()

    def _toggle_info(self):
        recd = self._active_recd
        if not recd:
            return

        if self._showing_info:
            self._show_recd(recd, play=False)
            return

        self._showing_info = True
        still_modes = (constants.MODE_PHOTO, constants.MODE_AUDIO)
        if self.model.get_mode() in still_modes:
            func = self._media_view.show_info_photo
        else:
            func = self._media_view.show_info_video

        self._play_button.hide()
        self._progress.hide()
        self._playback_scale.hide()
        self._title_entry.set_text(recd.title)
        self._title_entry.show()
        self._title_label.show()
        self.set_title_visible(True)

        func(recd.recorderName, recd.colorStroke, recd.colorFill,
             utils.getDateString(recd.time), recd.tags)

    def _media_view_fullscreen_clicked(self, widget):
        # logger.debug('_media_view_fullscreen_clicked')
        self._toggle_fullscreen()

    def _media_view_tags_changed(self, widget, tbuffer):
        text = tbuffer.get_text(tbuffer.get_start_iter(),
                                tbuffer.get_end_iter(), True)
        self._active_recd.setTags(text)

    def _toggle_fullscreen(self):
        # logger.debug('_toggle_fullscreen')
        self._fullscreen = not self._fullscreen

        if not self._active_recd:
            self.model.glive.stop()

        if self._fullscreen:
            self.get_toolbar_box().hide()
            self._thumb_tray.hide()
            if self._active_recd:
                self._controls_hbox.hide()
        else:
            self.get_toolbar_box().show()
            self._thumb_tray.show()
            self._controls_hbox.show()

        self._media_view.set_fullscreen(self._fullscreen)

        if self._active_recd:
            return

        if self.model.get_state() == constants.STATE_RECORDING:
            return

        # hack, reason unknown
        # problem: call to self.mode.glive.play() does not show live view
        # solution: defer until after VideoBox resize is complete

        self._timer_hid = None

        def on_timer_cb():
            self.model.glive.play()
            self._timer_hid = None
            return False

        self._timer_hid = GLib.timeout_add(1000, on_timer_cb)

        def on_defer_cb():
            self.model.glive.play()
            if self._timer_hid:
                GLib.source_remove(self._timer_hid)
                self._timer_hid = None
            return False

        def on_event_cb(widget, event):
            if event.state == Gdk.VisibilityState.UNOBSCURED:
                GLib.timeout_add(30, on_defer_cb)
                self._media_view._video.disconnect_by_func(on_event_cb)

        self._media_view._video.add_events(
            Gdk.EventMask.VISIBILITY_NOTIFY_MASK)
        self._media_view._video.connect('visibility-notify-event', on_event_cb)

        # FIXME: fullscreen toggle during video recording gives black
        # window, TODO: do the same as above for the video recording
        # pipeline when it is active

    def _mode_button_clicked(self, button):
        self.model.change_mode(button.mode)

    def _shutter_clicked(self, arg):
        self.model.do_shutter()

    def set_shutter_sensitive(self, value):
        self._shutter_button.set_sensitive(value)

    def set_state(self, state):
        radio_state = (state == constants.STATE_READY)
        for item in (self._photo_button,
                     self._audio_button,
                     self._video_button):
            if item:
                item.set_sensitive(radio_state)

        self._showing_info = False
        if state == constants.STATE_READY:
            if self._state == constants.STATE_PROCESSING:
                self.unbusy()
            self._active_recd = None
            self._mirror_btn.props.sensitive = True
            self._title_entry.hide()
            self._title_label.hide()
            self.set_title_visible(False)
            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.PackType.START)
            self._shutter_button.set_normal()
            self._shutter_button.set_sensitive(True)
            self._shutter_button.show()
            self._media_view.show_live()
        elif state == constants.STATE_RECORDING:
            self._mirror_btn.props.sensitive = False
            self._shutter_button.set_recording()
            self._controls_hbox.set_child_packing(self._shutter_button,
                                                  expand=False, fill=False,
                                                  padding=0,
                                                  pack_type=Gtk.PackType.START)
            self._progress.show()
        elif state == constants.STATE_PROCESSING:
            self.busy()
            self._shutter_button.hide()
            self._progress.show()
        elif state == constants.STATE_DOWNLOADING:
            self._shutter_button.hide()
            self._progress.show()
        self._state = state

    def set_paused(self, value):
        if value:
            self._play_button.set_play()
        else:
            self._play_button.set_pause()

    def _thumbnail_clicked(self, button, recd):
        if self.model.ui_frozen():
            return

        self.model.abort_countdown()
        self.model.glive.stop()
        self._mirror_btn.props.sensitive = False
        self._active_recd = recd
        self._show_recd(recd)

    def add_thumbnail(self, recd):
        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.handler_ids = (clicked_handler, remove_handler,
                              clipboard_handler)
        button.show()
        self._thumb_tray.add_item(button)
        self._thumb_tray.scroll_to_item(button)
        # FIXME: possible toolkit bug; scroll_to_item is ineffective,
        # only noticed when the tray is full

    def _copy_to_clipboard(self, recd):
        if recd is None:
            return
        if not recd.isClipboardCopyable():
            return

        clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
        clipboard.set_image(recd.getCopyClipboardPixbuf())

    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):
        for handler in recdbutton.handler_ids:
            recdbutton.disconnect(handler)

        self._thumb_tray.remove_item(recdbutton)
        recdbutton.cleanup()

    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.set_title_visible(True)
        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.set_title_visible(False)
        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.set_title_visible(False)
        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):
        self.model.set_state(constants.STATE_INVISIBLE)
        if recd == self._active_recd:
            self._show_recd(recd)

    def update_download_progress(self, recd):
        if recd != self._active_recd:
            return

        if not recd.meshDownloading:
            msg = _('Download failed.')
        elif recd.meshDownloadingProgress:
            msg = _('Downloading...')
        else:
            msg = _('Requesting...')

        self.set_progress(recd.meshDownlodingPercent, msg)
Example #2
0
class Chat(activity.Activity):
    def __init__(self, handle):
        pservice = presenceservice.get_instance()
        self.owner = pservice.get_owner()

        self._ebook_mode_detector = EbookModeDetector()

        self.chatbox = ChatBox(self.owner,
                               self._ebook_mode_detector.get_ebook_mode())
        self.chatbox.connect('open-on-journal', self.__open_on_journal)
        self.chatbox.connect('new-message',
                             self._search_entry_on_new_message_cb)

        super(Chat, self).__init__(handle)

        self._entry = None
        self._has_alert = False
        self._has_osk = False

        self._setup_canvas()

        self._entry.grab_focus()

        toolbar_box = ToolbarBox()
        self.set_toolbar_box(toolbar_box)

        self._activity_toolbar_button = ActivityToolbarButton(self)
        self._activity_toolbar_button.connect('clicked', self._fixed_resize_cb)

        toolbar_box.toolbar.insert(self._activity_toolbar_button, 0)
        self._activity_toolbar_button.show()

        self.search_entry = iconentry.IconEntry()
        self.search_entry.set_size_request(Gdk.Screen.width() / 3, -1)
        self.search_entry.set_icon_from_name(iconentry.ICON_ENTRY_PRIMARY,
                                             'entry-search')
        self.search_entry.add_clear_button()
        self.search_entry.connect('activate', self._search_entry_activate_cb)
        self.search_entry.connect('changed', self._search_entry_activate_cb)

        self.connect('key-press-event', self._search_entry_key_press_cb)

        self._search_item = Gtk.ToolItem()
        self._search_item.add(self.search_entry)
        toolbar_box.toolbar.insert(self._search_item, -1)

        self._search_prev = ToolButton('go-previous-paired')
        self._search_prev.set_tooltip(_('Previous'))
        self._search_prev.props.accelerator = "<Shift><Ctrl>g"
        self._search_prev.connect('clicked', self._search_prev_cb)
        self._search_prev.props.sensitive = False
        toolbar_box.toolbar.insert(self._search_prev, -1)

        self._search_next = ToolButton('go-next-paired')
        self._search_next.set_tooltip(_('Next'))
        self._search_next.props.accelerator = "<Ctrl>g"
        self._search_next.connect('clicked', self._search_next_cb)
        self._search_next.props.sensitive = False
        toolbar_box.toolbar.insert(self._search_next, -1)

        separator = Gtk.SeparatorToolItem()
        separator.props.draw = False
        separator.set_expand(True)
        toolbar_box.toolbar.insert(separator, -1)

        toolbar_box.toolbar.insert(StopButton(self), -1)
        toolbar_box.show_all()

        # Chat is room or one to one:
        self._chat_is_room = False
        self.text_channel = None

        if _HAS_SOUND:
            self.element = Gst.ElementFactory.make('playbin', 'Player')

        if self.shared_activity:
            # we are joining the activity following an invite
            self._alert(_('Off-line'), _('Joining the Chat.'))
            self._entry.props.placeholder_text = \
                _('Please wait for a connection before starting to chat.')
            self.connect('joined', self._joined_cb)
            if self.get_shared():
                # we have already joined
                self._joined_cb(self)
        elif handle.uri:
            # XMPP non-sugar3 incoming chat, not sharable
            self._activity_toolbar_button.props.page.share.props.visible = \
                False
            self._one_to_one_connection(handle.uri)
        else:
            # we are creating the activity
            if not self.metadata or self.metadata.get(
                    'share-scope', activity.SCOPE_PRIVATE) == \
                    activity.SCOPE_PRIVATE:
                # if we are in private session
                self._alert(_('Off-line'), _('Share, or invite someone.'))
            else:
                # resume of shared activity from journal object without invite
                self._entry.props.placeholder_text = \
                    _('Please wait for a connection before starting to chat.')
            self.connect('shared', self._shared_cb)

    def _search_entry_key_press_cb(self, activity, event):
        keyname = Gdk.keyval_name(event.keyval).lower()
        if keyname == 'f':
            if Gdk.ModifierType.CONTROL_MASK & event.state:
                self.search_entry.grab_focus()
        elif keyname == 'escape':
            self.search_entry.props.text = ''
            self._entry.grab_focus()

    def _search_entry_on_new_message_cb(self, chatbox):
        self._search_entry_activate_cb(self.search_entry)

    def _search_entry_activate_cb(self, entry):
        for i in range(0, self.chatbox.number_of_textboxes()):
            textbox = self.chatbox.get_textbox(i)
            _buffer = textbox.get_buffer()
            start_mark = _buffer.get_mark('start')
            end_mark = _buffer.get_mark('end')
            if start_mark is None or end_mark is None:
                continue
            _buffer.delete_mark(start_mark)
            _buffer.delete_mark(end_mark)
            self.chatbox.highlight_text = (None, None, None)
        self.chatbox.set_search_text(entry.props.text)
        self._update_search_buttons()

    def _update_search_buttons(self, ):
        if len(self.chatbox.search_text) == 0:
            self._search_prev.props.sensitive = False
            self._search_next.props.sensitive = False
        else:
            # If next or previous result exists
            self._search_prev.props.sensitive = \
                self.chatbox.check_next('backward')
            self._search_next.props.sensitive = \
                self.chatbox.check_next('forward')

    def _search_prev_cb(self, button):
        if button.props.sensitive:
            self.chatbox.search('backward')
            self._update_search_buttons()

    def _search_next_cb(self, button):
        if button.props.sensitive:
            self.chatbox.search('forward')
            self._update_search_buttons()

    def _fixed_resize_cb(self, widget=None, rect=None):
        ''' If a toolbar opens or closes, we need to resize the vbox
        holding out scrolling window. '''
        if self._has_alert:
            dy = style.GRID_CELL_SIZE
        else:
            dy = 0
        if self._has_osk:
            if Gdk.Screen.width() > Gdk.Screen.height():
                dy += OSK_HEIGHT[0]
            else:
                dy += OSK_HEIGHT[1]

        if self._toolbar_expanded():
            self.chatbox.set_size_request(
                self._chat_width,
                self._chat_height - style.GRID_CELL_SIZE - dy)
            self._fixed.move(self._entry_grid, style.GRID_CELL_SIZE,
                             self._chat_height - style.GRID_CELL_SIZE - dy)
        else:
            self.chatbox.set_size_request(self._chat_width,
                                          self._chat_height - dy)
            self._fixed.move(self._entry_grid, style.GRID_CELL_SIZE,
                             self._chat_height - dy)

        self.chatbox.resize_conversation(dy)

    def _setup_canvas(self):
        ''' Create a canvas '''
        self._fixed = Gtk.Fixed()
        self._fixed.set_size_request(
            Gdk.Screen.width(),
            Gdk.Screen.height() - style.GRID_CELL_SIZE)
        self._fixed.connect('size-allocate', self._fixed_resize_cb)
        self.set_canvas(self._fixed)
        self._fixed.show()

        self._entry_widgets = self._make_entry_widgets()
        self._fixed.put(self.chatbox, 0, 0)
        self.chatbox.show()

        self._fixed.put(self._entry_grid, style.GRID_CELL_SIZE,
                        self._chat_height)
        self._entry_grid.show()

        Gdk.Screen.get_default().connect('size-changed', self._configure_cb)

    def _configure_cb(self, event):
        self._fixed.set_size_request(
            Gdk.Screen.width(),
            Gdk.Screen.height() - style.GRID_CELL_SIZE)
        if self._ebook_mode_detector.get_ebook_mode():
            self._entry_height = int(style.GRID_CELL_SIZE * 1.5)
        else:
            self._entry_height = style.GRID_CELL_SIZE
        entry_width = Gdk.Screen.width() - \
            2 * (self._entry_height + style.GRID_CELL_SIZE)
        self._entry.set_size_request(entry_width, self._entry_height)
        self._entry_grid.set_size_request(
            Gdk.Screen.width() - 2 * style.GRID_CELL_SIZE, self._entry_height)

        self._chat_height = Gdk.Screen.height() - self._entry_height - \
            style.GRID_CELL_SIZE
        self._chat_width = Gdk.Screen.width()
        self.chatbox.set_size_request(self._chat_width, self._chat_height)
        self.chatbox.resize_all()

        width = int(Gdk.Screen.width() - 2 * style.GRID_CELL_SIZE)
        if self._ebook_mode_detector.get_ebook_mode():
            height = int(Gdk.Screen.height() - 8 * style.GRID_CELL_SIZE)
        else:
            height = int(Gdk.Screen.height() - 5 * style.GRID_CELL_SIZE)
        self._smiley_table.set_size_request(width, height)
        self._smiley_toolbar.set_size_request(width, -1)
        self._smiley_window.set_size_request(width, -1)

        self._fixed_resize_cb()

    def _create_smiley_table(self, width):
        pixel_size = (style.STANDARD_ICON_SIZE + style.LARGE_ICON_SIZE) / 2
        spacing = style.DEFAULT_SPACING
        button_size = pixel_size + spacing
        smilies_columns = int(width / button_size)
        pad = (width - smilies_columns * button_size) / 2

        table = Gtk.Grid()
        table.set_row_spacing(spacing)
        table.set_column_spacing(spacing)
        table.set_border_width(pad)

        queue = []

        def _create_smiley_icon_idle_cb():
            try:
                x, y, path, code = queue.pop()
            except IndexError:
                self.unbusy()
                return False
            pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
                path, pixel_size, pixel_size)
            image = Gtk.Image.new_from_pixbuf(pixbuf)
            box = Gtk.EventBox()
            box.add(image)
            box.connect('button-press-event', self._add_smiley_to_entry, code)
            table.attach(box, x, y, 1, 1)
            box.show_all()
            return True

        x = 0
        y = 0
        smilies.init()
        for i in range(len(smilies.THEME)):
            path, hint, codes = smilies.THEME[i]
            queue.append([x, y, path, codes[0]])

            x += 1
            if x == smilies_columns:
                y += 1
                x = 0

        queue.reverse()
        GLib.idle_add(_create_smiley_icon_idle_cb)
        return table

    def _add_smiley_to_entry(self, icon, event, text):
        pos = self._entry.props.cursor_position
        self._entry.insert_text(text, pos)
        self._entry.grab_focus()
        self._entry.set_position(pos + len(text))
        self._hide_smiley_window()

    def _shared_cb(self, sender):
        self._setup()

    def _one_to_one_connection(self, tp_channel):
        '''Handle a private invite from a non-sugar3 XMPP client.'''
        if self.shared_activity or self.text_channel:
            return
        bus_name, connection, channel = json.loads(tp_channel)
        logger.debug('GOT XMPP: %s %s %s', bus_name, connection, channel)
        conn = {}
        conn_proxy = dbus.Bus().get_object(bus_name, connection)
        conn[TelepathyGLib.IFACE_CONNECTION_INTERFACE_ALIASING] = \
            dbus.Interface(
                conn_proxy, TelepathyGLib.IFACE_CONNECTION_INTERFACE_ALIASING)
        self._one_to_one_connection_ready_cb(bus_name, channel, conn)

    def _one_to_one_connection_ready_cb(self, bus_name, channel, conn):
        '''Callback for Connection for one to one connection'''
        text_channel = {}
        text_proxy = dbus.Bus().get_object(bus_name, channel)
        text_channel[TelepathyGLib.IFACE_CHANNEL] = \
            dbus.Interface(text_proxy, TelepathyGLib.IFACE_CHANNEL)
        text_channel[TelepathyGLib.IFACE_CHANNEL_TYPE_TEXT] = \
            dbus.Interface(text_proxy, TelepathyGLib.IFACE_CHANNEL_TYPE_TEXT)
        text_channel[TelepathyGLib.IFACE_CHANNEL_INTERFACE_GROUP] = \
            dbus.Interface(
                text_proxy, TelepathyGLib.IFACE_CHANNEL_INTERFACE_GROUP)
        self.text_channel = TextChannelWrapper(text_channel, conn)
        self.text_channel.set_received_callback(self._received_cb)
        self.text_channel.handle_pending_messages()
        self.text_channel.set_closed_callback(
            self._one_to_one_connection_closed_cb)
        self._chat_is_room = False
        self._alert(_('On-line'), _('Private chat.'))

        # XXX How do we detect the sender going offline?
        self._entry.set_sensitive(True)
        self._entry.props.placeholder_text = None
        self._entry.grab_focus()

    def _one_to_one_connection_closed_cb(self):
        '''Callback for when the text channel closes.'''
        self._alert(_('Off-line'), _('Left the chat.'))

    def _setup(self):
        self.text_channel = TextChannelWrapper(
            self.shared_activity.telepathy_text_chan,
            self.shared_activity.telepathy_conn)
        self.text_channel.set_received_callback(self._received_cb)
        self._alert(_('On-line'), _('Connected.'))
        self.shared_activity.connect('buddy-joined', self._buddy_joined_cb)
        self.shared_activity.connect('buddy-left', self._buddy_left_cb)
        self._chat_is_room = True
        self._entry.set_sensitive(True)
        self._entry.props.placeholder_text = None
        self._entry.grab_focus()

    def _joined_cb(self, sender):
        '''Joined a shared activity.'''
        if not self.shared_activity:
            return
        logger.debug('Joined a shared chat')
        for buddy in self.shared_activity.get_joined_buddies():
            self._buddy_already_exists(buddy)
        self._setup()

    def _received_cb(self, buddy, text):
        '''Show message that was received.'''
        if buddy:
            if type(buddy) is dict:
                nick = buddy['nick']
            else:
                nick = buddy.props.nick
        else:
            nick = '???'
        logger.debug('Received message from %s: %s', nick, text)
        self.chatbox.add_text(buddy, text)

        if self.owner.props.nick in text:
            self.play_sound('said_nick')
        '''
        vscroll = self.chatbox.get_vadjustment()
        if vscroll.get_property('value') != vscroll.get_property('upper'):
            self._alert(_('New message'), _('New message from %s' % nick))
        '''
        if not self.has_focus:
            self.notify_user(_('Message from %s') % buddy, text)

    def _toolbar_expanded(self):
        if self._activity_toolbar_button.is_expanded():
            return True
        return False

    def _alert(self, title, text=None):
        alert = NotifyAlert(timeout=5)
        alert.props.title = title
        alert.props.msg = text
        self.add_alert(alert)
        alert.connect('response', self._alert_cancel_cb)
        alert.show()
        self._has_alert = True
        self._fixed_resize_cb()

    def _alert_cancel_cb(self, alert, response_id):
        self.remove_alert(alert)
        self._has_alert = False
        self._fixed_resize_cb()

    def __open_on_journal(self, widget, url):
        '''Ask the journal to display a URL'''
        logger.debug('Create journal entry for URL: %s', url)
        jobject = datastore.create()
        metadata = {
            'title': '%s: %s' % (_('URL from Chat'), url),
            'title_set_by_user': '******',
            'icon-color': profile.get_color().to_string(),
            'mime_type': 'text/uri-list',
        }
        for k, v in list(metadata.items()):
            jobject.metadata[k] = v
        file_path = os.path.join(get_activity_root(), 'instance',
                                 '%i_' % time.time())
        open(file_path, 'w').write(url + '\r\n')
        os.chmod(file_path, 0o755)
        jobject.set_file_path(file_path)
        datastore.write(jobject)
        show_object_in_journal(jobject.object_id)
        jobject.destroy()
        os.unlink(file_path)

    def _buddy_joined_cb(self, sender, buddy):
        '''Show a buddy who joined'''
        if buddy == self.owner:
            return
        self.chatbox.add_text(buddy,
                              _('%s joined the chat') % buddy.props.nick,
                              status_message=True)

        self.play_sound('login')

    def _buddy_left_cb(self, sender, buddy):
        '''Show a buddy who joined'''
        if buddy == self.owner:
            return
        self.chatbox.add_text(buddy,
                              _('%s left the chat') % buddy.props.nick,
                              status_message=True)

        self.play_sound('logout')

    def _buddy_already_exists(self, buddy):
        '''Show a buddy already in the chat.'''
        if buddy == self.owner:
            return
        self.chatbox.add_text(buddy,
                              _('%s is here') % buddy.props.nick,
                              status_message=True)

    def can_close(self):
        '''Perform cleanup before closing.
        Close text channel of a one to one XMPP chat.
        '''
        if self._chat_is_room is False:
            if self.text_channel is not None:
                self.text_channel.close()
        return True

    def _make_entry_widgets(self):
        '''We need to create a button for the smiley, a text entry, and a
        send button.

        All of this, along with the chatbox, goes into a grid.

        ---------------------------------------
        | chat box                            |
        | smiley button | entry | send button |
        ---------------------------------------
        '''
        if self._ebook_mode_detector.get_ebook_mode():
            self._entry_height = int(style.GRID_CELL_SIZE * 1.5)
        else:
            self._entry_height = style.GRID_CELL_SIZE
        entry_width = Gdk.Screen.width() - \
            2 * (self._entry_height + style.GRID_CELL_SIZE)
        self._chat_height = Gdk.Screen.height() - self._entry_height - \
            style.GRID_CELL_SIZE
        self._chat_width = Gdk.Screen.width()

        self.chatbox.set_size_request(self._chat_width, self._chat_height)

        self._entry_grid = Gtk.Grid()
        self._entry_grid.set_size_request(
            Gdk.Screen.width() - 2 * style.GRID_CELL_SIZE, self._entry_height)

        smiley_button = EventIcon(icon_name='smilies',
                                  pixel_size=self._entry_height)
        smiley_button.connect('button-press-event', self._smiley_button_cb)
        self._entry_grid.attach(smiley_button, 0, 0, 1, 1)
        smiley_button.show()

        self._entry = Gtk.Entry()
        self._entry.set_size_request(entry_width, self._entry_height)
        self._entry.modify_bg(Gtk.StateType.INSENSITIVE,
                              style.COLOR_WHITE.get_gdk_color())
        self._entry.modify_base(Gtk.StateType.INSENSITIVE,
                                style.COLOR_WHITE.get_gdk_color())

        self._entry.set_sensitive(False)
        self._entry.props.placeholder_text = \
            _('You must be connected to a friend before starting to chat.')
        self._entry.connect('focus-in-event', self._entry_focus_in_cb)
        self._entry.connect('focus-out-event', self._entry_focus_out_cb)
        self._entry.connect('activate', self._entry_activate_cb)
        self._entry.connect('key-press-event', self._entry_key_press_cb)
        self._entry_grid.attach(self._entry, 1, 0, 1, 1)
        self._entry.show()

        send_button = EventIcon(icon_name='send',
                                pixel_size=self._entry_height)
        send_button.connect('button-press-event', self._send_button_cb)
        self._entry_grid.attach(send_button, 2, 0, 1, 1)
        send_button.show()

    def _get_icon_pixbuf(self, name):
        icon_theme = Gtk.IconTheme.get_default()
        icon_info = icon_theme.lookup_icon(name, style.LARGE_ICON_SIZE, 0)
        pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
            icon_info.get_filename(), style.LARGE_ICON_SIZE,
            style.LARGE_ICON_SIZE)
        del icon_info
        return pixbuf

    def _entry_focus_in_cb(self, entry, event):
        self._hide_smiley_window()

        if self._ebook_mode_detector.get_ebook_mode():
            self._has_osk = True
            self._fixed_resize_cb()

    def _entry_focus_out_cb(self, entry, event):
        if self._ebook_mode_detector.get_ebook_mode():
            self._has_osk = False
            self._fixed_resize_cb()

    def _entry_key_press_cb(self, widget, event):
        '''Check for scrolling keys.

        Check if the user pressed Page Up, Page Down, Home or End and
        scroll the window according the pressed key.
        '''
        vadj = self.chatbox.get_vadjustment()
        if event.keyval == Gdk.KEY_Page_Down:
            value = vadj.get_value() + vadj.page_size
            if value > vadj.upper - vadj.page_size:
                value = vadj.upper - vadj.page_size
            vadj.set_value(value)
        elif event.keyval == Gdk.KEY_Page_Up:
            vadj.set_value(vadj.get_value() - vadj.page_size)
        elif event.keyval == Gdk.KEY_Home and \
                event.get_state() & Gdk.ModifierType.CONTROL_MASK:
            vadj.set_value(vadj.lower)
        elif event.keyval == Gdk.KEY_End and \
                event.get_state() & Gdk.ModifierType.CONTROL_MASK:
            vadj.set_value(vadj.upper - vadj.page_size)

    def _smiley_button_cb(self, widget, event):
        self._show_smiley_window()

    def _send_button_cb(self, widget, event):
        self._entry_activate_cb(self._entry)

    def _entry_activate_cb(self, entry):
        self.chatbox._scroll_auto = True

        text = entry.props.text
        if text:
            logger.debug('Adding text to chatbox: %s: %s' % (self.owner, text))
            self.chatbox.add_text(self.owner, text)
            entry.props.text = ''
            if self.text_channel:
                logger.debug('sending to text_channel: %s' % (text))
                self.text_channel.send(text)
            else:
                logger.debug('Tried to send message but text channel '
                             'not connected.')

    def write_file(self, file_path):
        '''Store chat log in Journal.

        Handling the Journal is provided by Activity - we only need
        to define this method.
        '''
        logger.debug('write_file: writing %s' % file_path)
        self.chatbox.add_log_timestamp()
        f = open(file_path, 'w')
        try:
            f.write(self.chatbox.get_log())
        finally:
            f.close()
        self.metadata['mime_type'] = 'text/plain'

    def read_file(self, file_path):
        '''Load a chat log from the Journal.
        Handling the Journal is provided by Activity - we only need
        to define this method.
        '''
        logger.debug('read_file: reading %s' % file_path)
        log = open(file_path).readlines()
        last_line_was_timestamp = False
        for line in log:
            if line.endswith('\t\t\n'):
                if last_line_was_timestamp is False:
                    timestamp = line.strip().split('\t')[0]
                    self.chatbox.add_separator(timestamp)
                    last_line_was_timestamp = True
            else:
                timestamp, nick, color, status, text = line.strip().split('\t')
                status_message = bool(int(status))
                self.chatbox.add_text({
                    'nick': nick,
                    'color': color
                }, text, status_message)
                last_line_was_timestamp = False

    def play_sound(self, event):
        if _HAS_SOUND:
            SOUNDS_PATH = os.path.join(get_bundle_path(), 'sounds')
            SOUNDS = {
                'said_nick': os.path.join(SOUNDS_PATH, 'alert.wav'),
                'login': os.path.join(SOUNDS_PATH, 'login.wav'),
                'logout': os.path.join(SOUNDS_PATH, 'logout.wav')
            }

            self.element.set_state(Gst.State.NULL)
            self.element.set_property('uri', 'file://%s' % SOUNDS[event])
            self.element.set_state(Gst.State.PLAYING)

    def _create_smiley_window(self):
        grid = Gtk.Grid()
        width = int(Gdk.Screen.width() - 2 * style.GRID_CELL_SIZE)

        self._smiley_toolbar = SmileyToolbar(self)
        height = style.GRID_CELL_SIZE
        self._smiley_toolbar.set_size_request(width, height)
        grid.attach(self._smiley_toolbar, 0, 0, 1, 1)
        self._smiley_toolbar.show()

        self._smiley_table = Gtk.ScrolledWindow()
        self._smiley_table.set_policy(Gtk.PolicyType.NEVER,
                                      Gtk.PolicyType.AUTOMATIC)
        self._smiley_table.modify_bg(Gtk.StateType.NORMAL,
                                     style.COLOR_BLACK.get_gdk_color())
        if self._ebook_mode_detector.get_ebook_mode():
            height = int(Gdk.Screen.height() - 8 * style.GRID_CELL_SIZE)
        else:
            height = int(Gdk.Screen.height() - 4 * style.GRID_CELL_SIZE)
        self._smiley_table.set_size_request(width, height)

        table = self._create_smiley_table(width)
        self._smiley_table.add_with_viewport(table)
        table.show_all()

        grid.attach(self._smiley_table, 0, 1, 1, 1)
        self._smiley_table.show()

        self._smiley_window = Gtk.ScrolledWindow()
        self._smiley_window.set_policy(Gtk.PolicyType.NEVER,
                                       Gtk.PolicyType.NEVER)
        self._smiley_window.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
        self._smiley_window.set_size_request(width, -1)

        self._smiley_window.add_with_viewport(grid)

        def _key_press_event_cb(widget, event):
            if event.keyval == Gdk.KEY_Escape:
                self._hide_smiley_window()
                return True
            return False

        self.connect('key-press-event', _key_press_event_cb)

        grid.show()

        self._fixed.put(self._smiley_window, style.GRID_CELL_SIZE, 0)

    def _show_smiley_window(self):
        if not hasattr(self, '_smiley_window'):
            self.busy()
            self._create_smiley_window()
        self._smiley_window.show()

    def _hide_smiley_window(self):
        if hasattr(self, '_smiley_window'):
            self._smiley_window.hide()
class JukeboxActivity(activity.Activity):

    __gsignals__ = {
        'playlist-finished': (GObject.SignalFlags.RUN_FIRST, None, []), }

    def __init__(self, handle):
        activity.Activity.__init__(self, handle)

        self.player = None

        self._alert = None
        self._playlist_jobject = None
        self._on_unfullscreen_show_playlist = False

        self.set_title(_('Jukebox Activity'))
        self.max_participants = 1

        toolbar_box = ToolbarBox()
        self._activity_toolbar_button = ActivityToolbarButton(self)
        activity_toolbar = self._activity_toolbar_button.page
        toolbar_box.toolbar.insert(self._activity_toolbar_button, 0)
        self.title_entry = activity_toolbar.title

        self._view_toolbar = ViewToolbar()
        self._view_toolbar.connect('go-fullscreen',
                                   self.__go_fullscreen_cb)
        self._view_toolbar.connect('toggle-playlist',
                                   self.__toggle_playlist_cb)
        view_toolbar_button = ToolbarButton(
            page=self._view_toolbar,
            icon_name='toolbar-view')
        self._view_toolbar.show()
        toolbar_box.toolbar.insert(view_toolbar_button, -1)
        view_toolbar_button.show()

        self._control_toolbar = Gtk.Toolbar()
        self._control_toolbar_button = ToolbarButton(
            page=self._control_toolbar,
            icon_name='media-playback-start')
        self._control_toolbar.show()
        toolbar_box.toolbar.insert(self._control_toolbar_button, -1)
        self._control_toolbar_button.hide()

        self.set_toolbar_box(toolbar_box)
        toolbar_box.show_all()

        self.connect('key_press_event', self.__key_press_event_cb)
        self.connect('playlist-finished', self.__playlist_finished_cb)

        # We want to be notified when the activity gets the focus or
        # loses it. When it is not active, we don't need to keep
        # reproducing the video
        self.connect('notify::active', self.__notify_active_cb)

        self._video_canvas = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)

        self._playlist_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)

        self.playlist_widget = PlayList()
        self.playlist_widget.connect('play-index', self.__play_index_cb)
        self.playlist_widget.connect('missing-tracks',
                                     self.__missing_tracks_cb)
        self.playlist_widget.set_size_request(
            Gdk.Screen.width() * PLAYLIST_WIDTH_PROP, 0)
        self.playlist_widget.show()

        self._playlist_box.pack_start(self.playlist_widget, expand=True,
                                      fill=True, padding=0)

        self._playlist_toolbar = Gtk.Toolbar()

        move_up = ToolButton("go-up")
        move_up.set_tooltip(_("Move up"))
        move_up.connect("clicked", self._move_up_cb)
        self._playlist_toolbar.insert(move_up, 0)

        move_down = ToolButton("go-down")
        move_down.set_tooltip(_("Move down"))
        move_down.connect("clicked", self._move_down_cb)
        self._playlist_toolbar.insert(move_down, 1)

        self._playlist_box.pack_end(self._playlist_toolbar, False, False, 0)
        self._video_canvas.pack_start(self._playlist_box, False, False, 0)

        # Create the player just once
        logging.debug('Instantiating GstPlayer')
        self.player = GstPlayer()
        self.player.connect('eos', self.__player_eos_cb)
        self.player.connect('error', self.__player_error_cb)
        self.player.connect('play', self.__player_play_cb)

        self.control = Controls(self, toolbar_box.toolbar,
                                self._control_toolbar)

        self._separator = Gtk.SeparatorToolItem()
        self._separator.props.draw = False
        self._separator.set_expand(True)
        self._separator.show()
        toolbar_box.toolbar.insert(self._separator, -1)

        self._stop = StopButton(self)
        toolbar_box.toolbar.insert(self._stop, -1)

        self._empty_widget = Gtk.Label(label="")
        self._empty_widget.show()
        self.videowidget = VideoWidget()
        self.set_canvas(self._video_canvas)
        self._init_view_area()
        self.show_all()

        if len(self.playlist_widget) < 2:
            self._view_toolbar._show_playlist.props.active = False

        self._configure_cb()

        self.player.init_view_area(self.videowidget)

        self._volume_monitor = Gio.VolumeMonitor.get()
        self._volume_monitor.connect('mount-added', self.__mount_added_cb)
        self._volume_monitor.connect('mount-removed', self.__mount_removed_cb)

        if handle.object_id is None:
            # The activity was launched from scratch. We need to show
            # the Empty Widget
            self.playlist_widget.hide()
            emptypanel.show(self, 'activity-jukebox',
                            _('No media'), _('Choose media files'),
                            self.control.show_picker_cb)

        self.control.check_if_next_prev()

        Gdk.Screen.get_default().connect('size-changed', self._configure_cb)

    def _move_up_cb(self, button):
        self.playlist_widget.move_up()

    def _move_down_cb(self, button):
        self.playlist_widget.move_down()

    def _configure_cb(self, event=None):
        toolbar = self.get_toolbar_box().toolbar
        toolbar.remove(self._stop)
        toolbar.remove(self._separator)
        if Gdk.Screen.width() < Gdk.Screen.height():
            self._control_toolbar_button.show()
            self._control_toolbar_button.set_expanded(True)
            self.control.update_layout(landscape=False)
            toolbar.insert(self._separator, -1)
        else:
            self._control_toolbar_button.set_expanded(False)
            self._control_toolbar_button.hide()
            self.control.update_layout(landscape=True)
        toolbar.insert(self._stop, -1)

    def __notify_active_cb(self, widget, event):
        """Sugar notify us that the activity is becoming active or inactive.
        When we are inactive, we stop the player if it is reproducing
        a video.
        """

        logging.debug('JukeboxActivity notify::active signal received')

        if self.player.player.props.current_uri is not None and \
                self.player.playing_video():
            if not self.player.is_playing() and self.props.active:
                self.player.play()
            if self.player.is_playing() and not self.props.active:
                self.player.pause()

    def _init_view_area(self):
        """
        Use a notebook with two pages, one empty an another
        with the videowidget
        """
        self.view_area = Gtk.Notebook()
        self.view_area.set_show_tabs(False)
        self.view_area.append_page(self._empty_widget, None)
        self.view_area.append_page(self.videowidget, None)
        self._video_canvas.pack_end(self.view_area, expand=True,
                                    fill=True, padding=0)

    def _switch_canvas(self, show_video):
        """Show or hide the video visualization in the canvas.

        When hidden, the canvas is filled with an empty widget to
        ensure redrawing.

        """
        if show_video:
            self.view_area.set_current_page(1)
        else:
            self.view_area.set_current_page(0)
        self._video_canvas.queue_draw()

    def __key_press_event_cb(self, widget, event):
        key = event.keyval
        ctrl = event.state & Gdk.ModifierType.CONTROL_MASK

        # while activity toolbar is visible, only escape key is taken
        if self._activity_toolbar_button.is_expanded():
            if key == Gdk.KEY_Escape:
                self._activity_toolbar_button.set_expanded(False)
                return True

            return False

        # while title is focused, no shortcuts
        if self.title_entry.has_focus():
            return False

        # Shortcut - Space does play or pause
        if key == Gdk.KEY_space:
            self.control.button.emit('clicked')
            return True

        # Shortcut - Up does previous playlist item
        if key == Gdk.KEY_Up:
            self.control.prev_button.emit('clicked')
            return True

        # Shortcut - Down does next playlist item
        if key == Gdk.KEY_Down:
            self.control.next_button.emit('clicked')
            return True

        # Shortcut - Escape does unfullscreen, then playlist hide
        if key == Gdk.KEY_Escape:
            if self.is_fullscreen():
                # sugar3.graphics.Window.__key_press_cb will handle it
                return False

            if self._view_toolbar._show_playlist.props.active:
                self._view_toolbar._show_playlist.props.active = False
                return True

        # Shortcut - ctrl-f does fullscreen toggle
        # (fullscreen enable is handled by ToolButton accelerator)
        if ctrl and key == Gdk.KEY_f:
            if self.is_fullscreen():
                self.unfullscreen()
                return True

        # Shortcut - ctrl-l does playlist toggle
        # (ToggleToolButton accelerator ineffective when ViewToolbar hidden)
        if ctrl and key == Gdk.KEY_l:
            togglebutton = self._view_toolbar._show_playlist
            togglebutton.props.active = not togglebutton.props.active
            return True

        return False

    def __playlist_finished_cb(self, widget):
        self._switch_canvas(show_video=False)
        self._view_toolbar._show_playlist.props.active = True
        self.unfullscreen()

        # Select the first stream to be played when Play button will
        # be pressed
        self.playlist_widget.set_current_playing(0)
        self.control.check_if_next_prev()

    def songchange(self, direction):
        current_playing = self.playlist_widget.get_current_playing()
        if direction == 'prev' and current_playing > 0:
            self.play_index(current_playing - 1)
        elif direction == 'next' and \
                current_playing < len(self.playlist_widget._items) - 1:
            self.play_index(current_playing + 1)
        else:
            self.emit('playlist-finished')

    def play_index(self, index):
        # README: this line is no more necessary because of the
        # .playing_video() method
        # self._switch_canvas(show_video=True)
        self.playlist_widget.set_current_playing(index)

        path = self.playlist_widget._items[index]['path']
        if self.playlist_widget.check_available_media(path):
            if self.playlist_widget.is_from_journal(path):
                path = self.playlist_widget.get_path_from_journal(path)
            self.control.check_if_next_prev()

            self.player.set_uri(path)
            self.player.play()
        else:
            self.songchange('next')

    def __play_index_cb(self, widget, index, path):
        # README: this line is no more necessary because of the
        # .playing_video() method
        # self._switch_canvas(show_video=True)
        self.playlist_widget.set_current_playing(index)

        if self.playlist_widget.is_from_journal(path):
            path = self.playlist_widget.get_path_from_journal(path)

        self.control.check_if_next_prev()

        self.player.set_uri(path)
        self.player.play()

    def __player_eos_cb(self, widget):
        self.songchange('next')

    def _show_error_alert(self, title, msg=None):
        self._alert = ErrorAlert()
        self._alert.props.title = title
        if msg is not None:
            self._alert.props.msg = msg
        self.add_alert(self._alert)
        self._alert.connect('response', self._alert_cancel_cb)
        self._alert.show()

    def __mount_added_cb(self, volume_monitor, device):
        logging.debug('Mountpoint added. Checking...')
        self.remove_alert(self._alert)
        self.playlist_widget.update()

    def __mount_removed_cb(self, volume_monitor, device):
        logging.debug('Mountpoint removed. Checking...')
        self.remove_alert(self._alert)
        self.playlist_widget.update()

    def __missing_tracks_cb(self, widget, tracks):
        self._show_missing_tracks_alert(tracks)

    def _show_missing_tracks_alert(self, tracks):
        self._alert = Alert()
        title = _('%s tracks not found.') % len(tracks)
        self._alert.props.title = title
        icon = Icon(icon_name='dialog-cancel')
        self._alert.add_button(Gtk.ResponseType.CANCEL, _('Dismiss'), icon)
        icon.show()

        icon = Icon(icon_name='dialog-ok')
        self._alert.add_button(Gtk.ResponseType.APPLY, _('Details'), icon)
        icon.show()
        self.add_alert(self._alert)
        self._alert.connect(
            'response', self.__missing_tracks_alert_response_cb, tracks)

    def __missing_tracks_alert_response_cb(self, alert, response_id, tracks):
        if response_id == Gtk.ResponseType.APPLY:
            vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
            vbox.props.valign = Gtk.Align.CENTER
            label = Gtk.Label(label='')
            label.set_markup(_('<b>Missing tracks</b>'))
            vbox.pack_start(label, False, False, 15)

            for track in tracks:
                label = Gtk.Label(label=track['path'])
                vbox.add(label)

            _missing_tracks = Gtk.ScrolledWindow()
            _missing_tracks.add_with_viewport(vbox)
            _missing_tracks.show_all()

            self.view_area.append_page(_missing_tracks, None)

            self.view_area.set_current_page(2)

        self.remove_alert(alert)

    def _alert_cancel_cb(self, alert, response_id):
        self.remove_alert(alert)

    def __player_play_cb(self, widget):
        self._switch_canvas(True)

    def __player_error_cb(self, widget, message, detail):
        self.player.stop()
        self.control.set_disabled()

        logging.error('ERROR MESSAGE: %s', message)
        logging.error('ERROR DETAIL: %s', detail)

        file_path = self.playlist_widget._items[
            self.playlist_widget.get_current_playing()]['path']
        mimetype = mime.get_for_file(file_path)

        title = _('Error')
        msg = _('This "%s" file can\'t be played') % mimetype
        self._switch_canvas(False)
        self._show_error_alert(title, msg)

    def can_close(self):
        # We need to put the Gst.State in NULL so gstreamer can
        # cleanup the pipeline
        self.player.stop()
        return True

    def read_file(self, file_path):
        """Load a file from the datastore on activity start."""
        logging.debug('JukeboxActivity.read_file: %s', file_path)

        title = self.metadata['title']
        self.playlist_widget.load_file(file_path, title)

    def write_file(self, file_path):

        def write_playlist_to_file(file_path):
            """Open the file at file_path and write the playlist.

            It is saved in audio/x-mpegurl format.

            """

            list_file = open(file_path, 'w')
            for uri in self.playlist_widget._items:
                list_file.write('#EXTINF:%s\n' % uri['title'])
                list_file.write('%s\n' % uri['path'])
            list_file.close()

        if not self.metadata['mime_type']:
            self.metadata['mime_type'] = 'audio/x-mpegurl'

        if self.metadata['mime_type'] == 'audio/x-mpegurl':
            write_playlist_to_file(file_path)

        else:
            if self._playlist_jobject is None:
                self._playlist_jobject = \
                    self.playlist_widget.create_playlist_jobject()

            # Add the playlist to the playlist jobject description.
            # This is only done if the activity was not started from a
            # playlist or from scratch:
            description = ''
            for uri in self.playlist_widget._items:
                description += '%s\n' % uri['title']
            self._playlist_jobject.metadata['description'] = description

            write_playlist_to_file(self._playlist_jobject.file_path)
            datastore.write(self._playlist_jobject)

    def unfullscreen(self):
        activity.Activity.unfullscreen(self)
        if self._on_unfullscreen_show_playlist:
            self._view_toolbar._show_playlist.props.active = True

    def __go_fullscreen_cb(self, toolbar):
        if self._view_toolbar._show_playlist.props.active:
            self._view_toolbar._show_playlist.props.active = False
            self._on_unfullscreen_show_playlist = True
        self.fullscreen()

    def __toggle_playlist_cb(self, toolbar):
        if self._view_toolbar._show_playlist.props.active:
            self._playlist_box.show_all()
        else:
            self._playlist_box.hide()
        self._video_canvas.queue_draw()
Example #4
0
class Chat(activity.Activity):

    def __init__(self, handle):
        smilies.init()

        pservice = presenceservice.get_instance()
        self.owner = pservice.get_owner()

        self._ebook_mode_detector = EbookModeDetector()

        self.chatbox = ChatBox(
            self.owner, self._ebook_mode_detector.get_ebook_mode())
        self.chatbox.connect('open-on-journal', self.__open_on_journal)

        super(Chat, self).__init__(handle)

        self._entry = None
        self._has_alert = False
        self._has_osk = False

        self._setup_canvas()
        GObject.idle_add(self._create_smiley_window)

        self._entry.grab_focus()

        toolbar_box = ToolbarBox()
        self.set_toolbar_box(toolbar_box)

        self._activity_toolbar_button = ActivityToolbarButton(self)
        self._activity_toolbar_button.connect('clicked', self._fixed_resize_cb)

        toolbar_box.toolbar.insert(self._activity_toolbar_button, 0)
        self._activity_toolbar_button.show()

        separator = Gtk.SeparatorToolItem()
        separator.props.draw = False
        separator.set_expand(True)
        toolbar_box.toolbar.insert(separator, -1)

        toolbar_box.toolbar.insert(StopButton(self), -1)
        toolbar_box.show_all()

        # Chat is room or one to one:
        self._chat_is_room = False
        self.text_channel = None

        if _HAS_SOUND:
            self.element = Gst.ElementFactory.make('playbin', 'Player')

        if self.shared_activity:
            # we are joining the activity
            self._entry.props.placeholder_text = \
                _('Please wait for a connection before starting to chat.')
            self.connect('joined', self._joined_cb)
            if self.get_shared():
                # we have already joined
                self._joined_cb(self)
        elif handle.uri:
            # XMPP non-sugar3 incoming chat, not sharable
            self._activity_toolbar_button.props.page.share.props.visible = \
                False
            self._one_to_one_connection(handle.uri)
        else:
            # we are creating the activity
            if not self.metadata or self.metadata.get(
                    'share-scope', activity.SCOPE_PRIVATE) == \
                    activity.SCOPE_PRIVATE:
                # if we are in private session
                self._alert(_('Off-line'), _('Share, or invite someone.'))
            else:
                self._entry.props.placeholder_text = \
                    _('Please wait for a connection before starting to chat.')
            self.connect('shared', self._shared_cb)

    def _fixed_resize_cb(self, widget=None, rect=None):
        ''' If a toolbar opens or closes, we need to resize the vbox
        holding out scrolling window. '''
        if self._has_alert:
            dy = style.GRID_CELL_SIZE
        else:
            dy = 0
        if self._has_osk:
            if Gdk.Screen.width() > Gdk.Screen.height():
                dy += OSK_HEIGHT[0]
            else:
                dy += OSK_HEIGHT[1]

        if self._toolbar_expanded():
            self.chatbox.set_size_request(
                self._chat_width,
                self._chat_height - style.GRID_CELL_SIZE - dy)
            self._fixed.move(self._entry_grid, style.GRID_CELL_SIZE,
                             self._chat_height - style.GRID_CELL_SIZE - dy)
        else:
            self.chatbox.set_size_request(self._chat_width,
                                          self._chat_height - dy)
            self._fixed.move(self._entry_grid, style.GRID_CELL_SIZE,
                             self._chat_height - dy)

        self.chatbox.resize_conversation(dy)

    def _setup_canvas(self):
        ''' Create a canvas '''
        self._fixed = Gtk.Fixed()
        self._fixed.set_size_request(
            Gdk.Screen.width(), Gdk.Screen.height() - style.GRID_CELL_SIZE)
        self._fixed.connect('size-allocate', self._fixed_resize_cb)
        self.set_canvas(self._fixed)
        self._fixed.show()

        self._entry_widgets = self._make_entry_widgets()
        self._fixed.put(self.chatbox, 0, 0)
        self.chatbox.show()

        self._fixed.put(self._entry_grid, style.GRID_CELL_SIZE,
                        self._chat_height)
        self._entry_grid.show()

        Gdk.Screen.get_default().connect('size-changed', self._configure_cb)

    def _configure_cb(self, event):
        self._fixed.set_size_request(
            Gdk.Screen.width(), Gdk.Screen.height() - style.GRID_CELL_SIZE)
        if self._ebook_mode_detector.get_ebook_mode():
            self._entry_height = int(style.GRID_CELL_SIZE * 1.5)
        else:
            self._entry_height = style.GRID_CELL_SIZE
        entry_width = Gdk.Screen.width() - \
                      2 * (self._entry_height + style.GRID_CELL_SIZE)
        self._entry.set_size_request(entry_width, self._entry_height)
        self._entry_grid.set_size_request(
            Gdk.Screen.width() - 2 * style.GRID_CELL_SIZE,
            self._entry_height)

        self._chat_height = Gdk.Screen.height() - self._entry_height - \
                                style.GRID_CELL_SIZE
        self._chat_width = Gdk.Screen.width()
        self.chatbox.set_size_request(self._chat_width, self._chat_height)
        self.chatbox.resize_all()

        width = int(Gdk.Screen.width() - 2 * style.GRID_CELL_SIZE)
        if self._ebook_mode_detector.get_ebook_mode():
            height = int(Gdk.Screen.height() - 8 * style.GRID_CELL_SIZE)
        else:
            height = int(Gdk.Screen.height() - 5 * style.GRID_CELL_SIZE)
        self._smiley_table.set_size_request(width, height)
        self._smiley_toolbar.set_size_request(width, -1)
        self._smiley_window.set_size_request(width, -1)

        self._fixed_resize_cb()

    def _create_smiley_table(self, width):
        button_size = style.STANDARD_ICON_SIZE + style.DEFAULT_PADDING
        width = width - 2 * style.DEFAULT_SPACING
        smilies_columns = int(width / button_size) - 2

        table = Gtk.Grid()
        table.set_row_spacing(0)
        table.set_column_spacing(0)
        table.set_border_width(style.DEFAULT_SPACING)

        x = 0
        y = 0
        for i in range(len(smilies.THEME)):
            path, hint, codes = smilies.THEME[i]
            image = Gtk.Image()
            image.set_from_file(path)
            button = Gtk.ToolButton()
            button.set_icon_widget(image)
            button.connect('clicked', self._add_smiley_to_entry, codes[0])
            table.attach(button, x, y, 1, 1)
            button.show()
            x += 1
            if x == smilies_columns:
                y += 1
                x = 0
        return table

    def _add_smiley_to_entry(self, button, text):
        pos = self._entry.props.cursor_position
        self._entry.insert_text(text, pos)
        self._entry.grab_focus()
        self._entry.set_position(pos + len(text))
        self._hide_smiley_window()

    def _shared_cb(self, sender):
        self._setup()

    def _one_to_one_connection(self, tp_channel):
        '''Handle a private invite from a non-sugar3 XMPP client.'''
        if self.shared_activity or self.text_channel:
            return
        bus_name, connection, channel = json.loads(tp_channel)
        logger.debug('GOT XMPP: %s %s %s', bus_name, connection, channel)
        Connection(bus_name, connection, ready_handler=lambda conn:
                   self._one_to_one_connection_ready_cb(
                       bus_name, channel, conn))

    def _one_to_one_connection_ready_cb(self, bus_name, channel, conn):
        '''Callback for Connection for one to one connection'''
        text_channel = Channel(bus_name, channel)
        self.text_channel = TextChannelWrapper(text_channel, conn)
        self.text_channel.set_received_callback(self._received_cb)
        self.text_channel.handle_pending_messages()
        self.text_channel.set_closed_callback(
            self._one_to_one_connection_closed_cb)
        self._chat_is_room = False
        self._alert(_('On-line'), _('Private Chat'))

        # XXX How do we detect the sender going offline?
        self._entry.set_sensitive(True)
        self._entry.props.placeholder_text = None
        self._entry.grab_focus()

    def _one_to_one_connection_closed_cb(self):
        '''Callback for when the text channel closes.'''
        self._alert(_('Off-line'), _('left the chat'))

    def _setup(self):
        self.text_channel = TextChannelWrapper(
            self.shared_activity.telepathy_text_chan,
            self.shared_activity.telepathy_conn)
        self.text_channel.set_received_callback(self._received_cb)
        self._alert(_('On-line'), _('Connected'))
        self.shared_activity.connect('buddy-joined', self._buddy_joined_cb)
        self.shared_activity.connect('buddy-left', self._buddy_left_cb)
        self._chat_is_room = True
        self._entry.set_sensitive(True)
        self._entry.props.placeholder_text = None
        self._entry.grab_focus()

    def _joined_cb(self, sender):
        '''Joined a shared activity.'''
        if not self.shared_activity:
            return
        logger.debug('Joined a shared chat')
        for buddy in self.shared_activity.get_joined_buddies():
            self._buddy_already_exists(buddy)
        self._setup()

    def _received_cb(self, buddy, text):
        '''Show message that was received.'''
        if buddy:
            if type(buddy) is dict:
                nick = buddy['nick']
            else:
                nick = buddy.props.nick
        else:
            nick = '???'
        logger.debug('Received message from %s: %s', nick, text)
        self.chatbox.add_text(buddy, text)

        if self.owner.props.nick in text:
            self.play_sound('said_nick')

        '''
        vscroll = self.chatbox.get_vadjustment()
        if vscroll.get_property('value') != vscroll.get_property('upper'):
            self._alert(_('New message'), _('New message from %s' % nick))
        '''
        if not self.has_focus:
            self.notify_user(_('Message from %s') % buddy, text)

    def _toolbar_expanded(self):
        if self._activity_toolbar_button.is_expanded():
            return True
        return False

    def _alert(self, title, text=None):
        alert = NotifyAlert(timeout=5)
        alert.props.title = title
        alert.props.msg = text
        self.add_alert(alert)
        alert.connect('response', self._alert_cancel_cb)
        alert.show()
        self._has_alert = True
        self._fixed_resize_cb()

    def _alert_cancel_cb(self, alert, response_id):
        self.remove_alert(alert)
        self._has_alert = False
        self._fixed_resize_cb()

    def __open_on_journal(self, widget, url):
        '''Ask the journal to display a URL'''
        logger.debug('Create journal entry for URL: %s', url)
        jobject = datastore.create()
        metadata = {
            'title': '%s: %s' % (_('URL from Chat'), url),
            'title_set_by_user': '******',
            'icon-color': profile.get_color().to_string(),
            'mime_type': 'text/uri-list',
            }
        for k, v in metadata.items():
            jobject.metadata[k] = v
        file_path = os.path.join(get_activity_root(), 'instance',
                                 '%i_' % time.time())
        open(file_path, 'w').write(url + '\r\n')
        os.chmod(file_path, 0755)
        jobject.set_file_path(file_path)
        datastore.write(jobject)
        show_object_in_journal(jobject.object_id)
        jobject.destroy()
        os.unlink(file_path)

    def _buddy_joined_cb(self, sender, buddy):
        '''Show a buddy who joined'''
        if buddy == self.owner:
            return
        self.chatbox.add_text(
            buddy, _('%s joined the chat') % buddy.props.nick,
            status_message=True)

        self.play_sound('login')

    def _buddy_left_cb(self, sender, buddy):
        '''Show a buddy who joined'''
        if buddy == self.owner:
            return
        self.chatbox.add_text(
            buddy, _('%s left the chat') % buddy.props.nick,
            status_message=True)

        self.play_sound('logout')

    def _buddy_already_exists(self, buddy):
        '''Show a buddy already in the chat.'''
        if buddy == self.owner:
            return
        self.chatbox.add_text(
            buddy, _('%s is here') % buddy.props.nick,
            status_message=True)

    def can_close(self):
        '''Perform cleanup before closing.
        Close text channel of a one to one XMPP chat.
        '''
        if self._chat_is_room is False:
            if self.text_channel is not None:
                self.text_channel.close()
        return True

    def _make_entry_widgets(self):
        '''We need to create a button for the smiley, a text entry, and a
        send button.

        All of this, along with the chatbox, goes into a grid.

        ---------------------------------------
        | chat box                            |
        | smiley button | entry | send button |
        ---------------------------------------
        '''
        if self._ebook_mode_detector.get_ebook_mode():
            self._entry_height = int(style.GRID_CELL_SIZE * 1.5)
        else:
            self._entry_height = style.GRID_CELL_SIZE
        entry_width = Gdk.Screen.width() - \
                      2 * (self._entry_height + style.GRID_CELL_SIZE)
        self._chat_height = Gdk.Screen.height() - self._entry_height - \
                            style.GRID_CELL_SIZE
        self._chat_width = Gdk.Screen.width()

        self.chatbox.set_size_request(self._chat_width, self._chat_height)

        self._entry_grid = Gtk.Grid()
        self._entry_grid.set_size_request(
            Gdk.Screen.width() - 2 * style.GRID_CELL_SIZE,
            self._entry_height)

        smiley_button = EventIcon(icon_name='smilies',
                                  pixel_size=self._entry_height)
        smiley_button.connect('button-press-event', self._smiley_button_cb)
        self._entry_grid.attach(smiley_button, 0, 0, 1, 1)
        smiley_button.show()

        self._entry = Gtk.Entry()
        self._entry.set_size_request(entry_width, self._entry_height)
        self._entry.modify_bg(Gtk.StateType.INSENSITIVE,
                              style.COLOR_WHITE.get_gdk_color())
        self._entry.modify_base(Gtk.StateType.INSENSITIVE,
                                style.COLOR_WHITE.get_gdk_color())

        self._entry.set_sensitive(False)
        self._entry.props.placeholder_text = \
            _('You must be connected to a friend before starting to chat.')

        self._entry.connect('focus-in-event', self._entry_focus_in_cb)
        self._entry.connect('focus-out-event', self._entry_focus_out_cb)
        self._entry.connect('activate', self._entry_activate_cb)
        self._entry.connect('key-press-event', self._entry_key_press_cb)
        self._entry_grid.attach(self._entry, 1, 0, 1, 1)
        self._entry.show()

        send_button = EventIcon(icon_name='send',
                                pixel_size=self._entry_height)
        send_button.connect('button-press-event', self._send_button_cb)
        self._entry_grid.attach(send_button, 2, 0, 1, 1)
        send_button.show()

    def _get_icon_pixbuf(self, name):
        icon_theme = Gtk.IconTheme.get_default()
        icon_info = icon_theme.lookup_icon(
            name, style.LARGE_ICON_SIZE, 0)
        pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
            icon_info.get_filename(), style.LARGE_ICON_SIZE,
            style.LARGE_ICON_SIZE)
        del icon_info
        return pixbuf

    def _entry_focus_in_cb(self, entry, event):
        self._hide_smiley_window()

        if self._ebook_mode_detector.get_ebook_mode():
            self._has_osk = True
            self._fixed_resize_cb()

    def _entry_focus_out_cb(self, entry, event):
        if self._ebook_mode_detector.get_ebook_mode():
            self._has_osk = False
            self._fixed_resize_cb()

    def _entry_key_press_cb(self, widget, event):
        '''Check for scrolling keys.

        Check if the user pressed Page Up, Page Down, Home or End and
        scroll the window according the pressed key.
        '''
        vadj = self.chatbox.get_vadjustment()
        if event.keyval == Gdk.KEY_Page_Down:
            value = vadj.get_value() + vadj.page_size
            if value > vadj.upper - vadj.page_size:
                value = vadj.upper - vadj.page_size
            vadj.set_value(value)
        elif event.keyval == Gdk.KEY_Page_Up:
            vadj.set_value(vadj.get_value() - vadj.page_size)
        elif event.keyval == Gdk.KEY_Home and \
             event.get_state() & Gdk.ModifierType.CONTROL_MASK:
                vadj.set_value(vadj.lower)
        elif event.keyval == Gdk.KEY_End and \
             event.get_state() & Gdk.ModifierType.CONTROL_MASK:
                vadj.set_value(vadj.upper - vadj.page_size)

    def _smiley_button_cb(self, widget, event):
        self._show_smiley_window()

    def _send_button_cb(self, widget, event):
        self._entry_activate_cb(self._entry)

    def _entry_activate_cb(self, entry):
        self.chatbox._scroll_auto = True

        text = entry.props.text
        if text:
            logger.debug('Adding text to chatbox: %s: %s' % (self.owner, text))
            self.chatbox.add_text(self.owner, text)
            entry.props.text = ''
            if self.text_channel:
                logger.debug('sending to text_channel: %s' % (text))
                self.text_channel.send(text)
            else:
                logger.debug('Tried to send message but text channel '
                             'not connected.')

    def write_file(self, file_path):
        '''Store chat log in Journal.

        Handling the Journal is provided by Activity - we only need
        to define this method.
        '''
        logger.debug('write_file: writing %s' % file_path)
        self.chatbox.add_log_timestamp()
        f = open(file_path, 'w')
        try:
            f.write(self.chatbox.get_log())
        finally:
            f.close()
        self.metadata['mime_type'] = 'text/plain'

    def read_file(self, file_path):
        '''Load a chat log from the Journal.
        Handling the Journal is provided by Activity - we only need
        to define this method.
        '''
        logger.debug('read_file: reading %s' % file_path)
        log = open(file_path).readlines()
        last_line_was_timestamp = False
        for line in log:
            if line.endswith('\t\t\n'):
                if last_line_was_timestamp is False:
                    timestamp = line.strip().split('\t')[0]
                    self.chatbox.add_separator(timestamp)
                    last_line_was_timestamp = True
            else:
                timestamp, nick, color, status, text = line.strip().split('\t')
                status_message = bool(int(status))
                self.chatbox.add_text({'nick': nick, 'color': color},
                                      text, status_message)
                last_line_was_timestamp = False

    def play_sound(self, event):
        if _HAS_SOUND:
            SOUNDS_PATH = os.path.join(get_bundle_path(), 'sounds')
            SOUNDS = {'said_nick': os.path.join(SOUNDS_PATH, 'alert.wav'),
                      'login': os.path.join(SOUNDS_PATH, 'login.wav'),
                      'logout': os.path.join(SOUNDS_PATH, 'logout.wav')}

            self.element.set_state(Gst.State.NULL)
            self.element.set_property('uri', 'file://%s' % SOUNDS[event])
            self.element.set_state(Gst.State.PLAYING)

    def _create_smiley_window(self):
        grid = Gtk.Grid()
        width = int(Gdk.Screen.width() - 2 * style.GRID_CELL_SIZE)

        self._smiley_toolbar = SmileyToolbar(self)
        height = style.GRID_CELL_SIZE
        self._smiley_toolbar.set_size_request(width, height)
        grid.attach(self._smiley_toolbar, 0, 0, 1, 1)
        self._smiley_toolbar.show()

        self._smiley_table = Gtk.ScrolledWindow()
        self._smiley_table.set_policy(Gtk.PolicyType.AUTOMATIC,
                                      Gtk.PolicyType.AUTOMATIC)
        self._smiley_table.modify_bg(
            Gtk.StateType.NORMAL, style.COLOR_BLACK.get_gdk_color())
        if self._ebook_mode_detector.get_ebook_mode():
            height = int(Gdk.Screen.height() - 8 * style.GRID_CELL_SIZE)
        else:
            height = int(Gdk.Screen.height() - 5 * style.GRID_CELL_SIZE)
        self._smiley_table.set_size_request(width, height)

        table = self._create_smiley_table(width)
        self._smiley_table.add_with_viewport(table)
        table.show_all()

        grid.attach(self._smiley_table, 0, 1, 1, 1)
        self._smiley_table.show()

        self._smiley_window = Gtk.ScrolledWindow()
        self._smiley_window.set_policy(Gtk.PolicyType.NEVER,
                                       Gtk.PolicyType.NEVER)
        self._smiley_window.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
        self._smiley_window.set_size_request(width, -1)

        self._smiley_window.add_with_viewport(grid)
        grid.show()

        self._fixed.put(self._smiley_window, style.GRID_CELL_SIZE, 0)

    def _show_smiley_window(self):
        self._smiley_window.show()

    def _hide_smiley_window(self):
        self._smiley_window.hide()
Example #5
0
class JukeboxActivity(activity.Activity):

    __gsignals__ = {
        'playlist-finished': (GObject.SignalFlags.RUN_FIRST, None, []), }

    def __init__(self, handle):
        activity.Activity.__init__(self, handle)

        self.player = None

        self._alert = None
        self._playlist_jobject = None
        self._on_unfullscreen_show_playlist = False

        self.set_title(_('Jukebox Activity'))
        self.max_participants = 1

        toolbar_box = ToolbarBox()
        self._activity_toolbar_button = ActivityToolbarButton(self)
        activity_toolbar = self._activity_toolbar_button.page
        toolbar_box.toolbar.insert(self._activity_toolbar_button, 0)
        self.title_entry = activity_toolbar.title

        self._view_toolbar = ViewToolbar()
        self._view_toolbar.connect('go-fullscreen',
                                   self.__go_fullscreen_cb)
        self._view_toolbar.connect('toggle-playlist',
                                   self.__toggle_playlist_cb)
        view_toolbar_button = ToolbarButton(
            page=self._view_toolbar,
            icon_name='toolbar-view')
        self._view_toolbar.show()
        toolbar_box.toolbar.insert(view_toolbar_button, -1)
        view_toolbar_button.show()

        self._control_toolbar = Gtk.Toolbar()
        self._control_toolbar_button = ToolbarButton(
            page=self._control_toolbar,
            icon_name='media-playback-start')
        self._control_toolbar.show()
        toolbar_box.toolbar.insert(self._control_toolbar_button, -1)
        self._control_toolbar_button.hide()

        self.set_toolbar_box(toolbar_box)
        toolbar_box.show_all()

        self.connect('key_press_event', self.__key_press_event_cb)
        self.connect('playlist-finished', self.__playlist_finished_cb)

        # We want to be notified when the activity gets the focus or
        # loses it. When it is not active, we don't need to keep
        # reproducing the video
        self.connect('notify::active', self.__notify_active_cb)

        self._video_canvas = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)

        self._playlist_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)

        self.playlist_widget = PlayList()
        self.playlist_widget.connect('play-index', self.__play_index_cb)
        self.playlist_widget.connect('missing-tracks',
                                     self.__missing_tracks_cb)
        self.playlist_widget.set_size_request(
            Gdk.Screen.width() * PLAYLIST_WIDTH_PROP, 0)
        self.playlist_widget.show()

        self._playlist_box.pack_start(self.playlist_widget, expand=True,
                                      fill=True, padding=0)

        self._playlist_toolbar = Gtk.Toolbar()

        move_up = ToolButton("go-up")
        move_up.set_tooltip(_("Move up"))
        move_up.connect("clicked", self._move_up_cb)
        self._playlist_toolbar.insert(move_up, 0)

        move_down = ToolButton("go-down")
        move_down.set_tooltip(_("Move down"))
        move_down.connect("clicked", self._move_down_cb)
        self._playlist_toolbar.insert(move_down, 1)

        self._playlist_box.pack_end(self._playlist_toolbar, False, False, 0)
        self._video_canvas.pack_start(self._playlist_box, False, False, 0)

        # Create the player just once
        logging.debug('Instantiating GstPlayer')
        self.player = GstPlayer()
        self.player.connect('eos', self.__player_eos_cb)
        self.player.connect('error', self.__player_error_cb)
        self.player.connect('play', self.__player_play_cb)

        self.control = Controls(self, toolbar_box.toolbar,
                                self._control_toolbar)

        self._separator = Gtk.SeparatorToolItem()
        self._separator.props.draw = False
        self._separator.set_expand(True)
        self._separator.show()
        toolbar_box.toolbar.insert(self._separator, -1)

        self._stop = StopButton(self)
        toolbar_box.toolbar.insert(self._stop, -1)

        self._empty_widget = Gtk.Label(label="")
        self._empty_widget.show()
        self.videowidget = VideoWidget()
        self.set_canvas(self._video_canvas)
        self._init_view_area()
        self.show_all()

        if len(self.playlist_widget) < 2:
            self._view_toolbar._show_playlist.props.active = False

        self._configure_cb()

        self.player.init_view_area(self.videowidget)

        self._volume_monitor = Gio.VolumeMonitor.get()
        self._volume_monitor.connect('mount-added', self.__mount_added_cb)
        self._volume_monitor.connect('mount-removed', self.__mount_removed_cb)

        if handle.object_id is None:
            # The activity was launched from scratch. We need to show
            # the Empty Widget
            self.playlist_widget.hide()
            emptypanel.show(self, 'activity-jukebox',
                            _('No media'), _('Choose media files'),
                            self.control.show_picker_cb)

        self.control.check_if_next_prev()

        Gdk.Screen.get_default().connect('size-changed', self._configure_cb)

    def _move_up_cb(self, button):
        self.playlist_widget.move_up()

    def _move_down_cb(self, button):
        self.playlist_widget.move_down()

    def _configure_cb(self, event=None):
        toolbar = self.get_toolbar_box().toolbar
        toolbar.remove(self._stop)
        toolbar.remove(self._separator)
        if Gdk.Screen.width() < Gdk.Screen.height():
            self._control_toolbar_button.show()
            self._control_toolbar_button.set_expanded(True)
            self.control.update_layout(landscape=False)
            toolbar.insert(self._separator, -1)
        else:
            self._control_toolbar_button.set_expanded(False)
            self._control_toolbar_button.hide()
            self.control.update_layout(landscape=True)
        toolbar.insert(self._stop, -1)

    def __notify_active_cb(self, widget, event):
        """Sugar notify us that the activity is becoming active or inactive.
        When we are inactive, we stop the player if it is reproducing
        a video.
        """

        logging.debug('JukeboxActivity notify::active signal received')

        if self.player.player.props.current_uri is not None and \
                self.player.playing_video():
            if not self.player.is_playing() and self.props.active:
                self.player.play()
            if self.player.is_playing() and not self.props.active:
                self.player.pause()

    def _init_view_area(self):
        """
        Use a notebook with two pages, one empty an another
        with the videowidget
        """
        self.view_area = Gtk.Notebook()
        self.view_area.set_show_tabs(False)
        self.view_area.append_page(self._empty_widget, None)
        self.view_area.append_page(self.videowidget, None)
        self._video_canvas.pack_end(self.view_area, expand=True,
                                    fill=True, padding=0)

    def _switch_canvas(self, show_video):
        """Show or hide the video visualization in the canvas.

        When hidden, the canvas is filled with an empty widget to
        ensure redrawing.

        """
        if show_video:
            self.view_area.set_current_page(1)
        else:
            self.view_area.set_current_page(0)
        self._video_canvas.queue_draw()

    def __key_press_event_cb(self, widget, event):
        key = event.keyval
        ctrl = event.state & Gdk.ModifierType.CONTROL_MASK

        # while activity toolbar is visible, only escape key is taken
        if self._activity_toolbar_button.is_expanded():
            if key == Gdk.KEY_Escape:
                self._activity_toolbar_button.set_expanded(False)
                return True

            return False

        # while title is focused, no shortcuts
        if self.title_entry.has_focus():
            return False

        # Shortcut - Space does play or pause
        if key == Gdk.KEY_space:
            self.control.button.emit('clicked')
            return True

        # Shortcut - Up does previous playlist item
        if key == Gdk.KEY_Up:
            self.control.prev_button.emit('clicked')
            return True

        # Shortcut - Down does next playlist item
        if key == Gdk.KEY_Down:
            self.control.next_button.emit('clicked')
            return True

        # Shortcut - Escape does unfullscreen, then playlist hide
        if key == Gdk.KEY_Escape:
            if self.is_fullscreen():
                # sugar3.graphics.Window.__key_press_cb will handle it
                return False

            if self._view_toolbar._show_playlist.props.active:
                self._view_toolbar._show_playlist.props.active = False
                return True

        # Shortcut - ctrl-f does fullscreen toggle
        # (fullscreen enable is handled by ToolButton accelerator)
        if ctrl and key == Gdk.KEY_f:
            if self.is_fullscreen():
                self.unfullscreen()
                return True

        # Shortcut - ctrl-l does playlist toggle
        # (ToggleToolButton accelerator ineffective when ViewToolbar hidden)
        if ctrl and key == Gdk.KEY_l:
            togglebutton = self._view_toolbar._show_playlist
            togglebutton.props.active = not togglebutton.props.active
            return True

        return False

    def __playlist_finished_cb(self, widget):
        self._switch_canvas(show_video=False)
        self._view_toolbar._show_playlist.props.active = True
        self.unfullscreen()

        # Select the first stream to be played when Play button will
        # be pressed
        self.playlist_widget.set_current_playing(0)
        self.control.check_if_next_prev()

    def songchange(self, direction):
        current_playing = self.playlist_widget.get_current_playing()
        if direction == 'prev' and current_playing > 0:
            self.play_index(current_playing - 1)
        elif direction == 'next' and \
                current_playing < len(self.playlist_widget._items) - 1:
            self.play_index(current_playing + 1)
        else:
            self.emit('playlist-finished')

    def play_index(self, index):
        # README: this line is no more necessary because of the
        # .playing_video() method
        # self._switch_canvas(show_video=True)
        self.playlist_widget.set_current_playing(index)

        path = self.playlist_widget._items[index]['path']
        if self.playlist_widget.check_available_media(path):
            if self.playlist_widget.is_from_journal(path):
                path = self.playlist_widget.get_path_from_journal(path)
            self.control.check_if_next_prev()

            self.player.set_uri(path)
            self.player.play()
        else:
            self.songchange('next')

    def __play_index_cb(self, widget, index, path):
        # README: this line is no more necessary because of the
        # .playing_video() method
        # self._switch_canvas(show_video=True)
        self.playlist_widget.set_current_playing(index)

        if self.playlist_widget.is_from_journal(path):
            path = self.playlist_widget.get_path_from_journal(path)

        self.control.check_if_next_prev()

        self.player.set_uri(path)
        self.player.play()

    def __player_eos_cb(self, widget):
        self.songchange('next')

    def _show_error_alert(self, title, msg=None):
        self._alert = ErrorAlert()
        self._alert.props.title = title
        if msg is not None:
            self._alert.props.msg = msg
        self.add_alert(self._alert)
        self._alert.connect('response', self._alert_cancel_cb)
        self._alert.show()

    def __mount_added_cb(self, volume_monitor, device):
        logging.debug('Mountpoint added. Checking...')
        self.remove_alert(self._alert)
        self.playlist_widget.update()

    def __mount_removed_cb(self, volume_monitor, device):
        logging.debug('Mountpoint removed. Checking...')
        self.remove_alert(self._alert)
        self.playlist_widget.update()

    def __missing_tracks_cb(self, widget, tracks):
        self._show_missing_tracks_alert(tracks)

    def _show_missing_tracks_alert(self, tracks):
        self._alert = Alert()
        title = _('%s tracks not found.') % len(tracks)
        self._alert.props.title = title
        icon = Icon(icon_name='dialog-cancel')
        self._alert.add_button(Gtk.ResponseType.CANCEL, _('Dismiss'), icon)
        icon.show()

        icon = Icon(icon_name='dialog-ok')
        self._alert.add_button(Gtk.ResponseType.APPLY, _('Details'), icon)
        icon.show()
        self.add_alert(self._alert)
        self._alert.connect(
            'response', self.__missing_tracks_alert_response_cb, tracks)

    def __missing_tracks_alert_response_cb(self, alert, response_id, tracks):
        if response_id == Gtk.ResponseType.APPLY:
            vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
            vbox.props.valign = Gtk.Align.CENTER
            label = Gtk.Label(label='')
            label.set_markup(_('<b>Missing tracks</b>'))
            vbox.pack_start(label, False, False, 15)

            for track in tracks:
                label = Gtk.Label(label=track['path'])
                vbox.add(label)

            _missing_tracks = Gtk.ScrolledWindow()
            _missing_tracks.add_with_viewport(vbox)
            _missing_tracks.show_all()

            self.view_area.append_page(_missing_tracks, None)

            self.view_area.set_current_page(2)

        self.remove_alert(alert)

    def _alert_cancel_cb(self, alert, response_id):
        self.remove_alert(alert)

    def __player_play_cb(self, widget):
        # Do not show the visualization widget if we are playing just
        # an audio stream

        def callback():
            if self.player.playing_video():
                self._switch_canvas(True)
            else:
                self._switch_canvas(False)
            return False

        # HACK: we need a timeout here because gstreamer returns
        # n-video = 0 if we call it immediately
        GObject.timeout_add(1000, callback)

    def __player_error_cb(self, widget, message, detail):
        self.player.stop()
        self.control.set_disabled()

        logging.error('ERROR MESSAGE: %s', message)
        logging.error('ERROR DETAIL: %s', detail)

        file_path = self.playlist_widget._items[
            self.playlist_widget.get_current_playing()]['path']
        mimetype = mime.get_for_file(file_path)

        title = _('Error')
        msg = _('This "%s" file can\'t be played') % mimetype
        self._switch_canvas(False)
        self._show_error_alert(title, msg)

    def can_close(self):
        # We need to put the Gst.State in NULL so gstreamer can
        # cleanup the pipeline
        self.player.stop()
        return True

    def read_file(self, file_path):
        """Load a file from the datastore on activity start."""
        logging.debug('JukeboxActivity.read_file: %s', file_path)

        title = self.metadata['title']
        self.playlist_widget.load_file(file_path, title)

    def write_file(self, file_path):

        def write_playlist_to_file(file_path):
            """Open the file at file_path and write the playlist.

            It is saved in audio/x-mpegurl format.

            """

            list_file = open(file_path, 'w')
            for uri in self.playlist_widget._items:
                list_file.write('#EXTINF:%s\n' % uri['title'])
                list_file.write('%s\n' % uri['path'])
            list_file.close()

        if not self.metadata['mime_type']:
            self.metadata['mime_type'] = 'audio/x-mpegurl'

        if self.metadata['mime_type'] == 'audio/x-mpegurl':
            write_playlist_to_file(file_path)

        else:
            if self._playlist_jobject is None:
                self._playlist_jobject = \
                    self.playlist_widget.create_playlist_jobject()

            # Add the playlist to the playlist jobject description.
            # This is only done if the activity was not started from a
            # playlist or from scratch:
            description = ''
            for uri in self.playlist_widget._items:
                description += '%s\n' % uri['title']
            self._playlist_jobject.metadata['description'] = description

            write_playlist_to_file(self._playlist_jobject.file_path)
            datastore.write(self._playlist_jobject)

    def unfullscreen(self):
        activity.Activity.unfullscreen(self)
        if self._on_unfullscreen_show_playlist:
            self._view_toolbar._show_playlist.props.active = True

    def __go_fullscreen_cb(self, toolbar):
        if self._view_toolbar._show_playlist.props.active:
            self._view_toolbar._show_playlist.props.active = False
            self._on_unfullscreen_show_playlist = True
        self.fullscreen()

    def __toggle_playlist_cb(self, toolbar):
        if self._view_toolbar._show_playlist.props.active:
            self._playlist_box.show_all()
        else:
            self._playlist_box.hide()
        self._video_canvas.queue_draw()