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