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)
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()
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) 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_button = ActivityButton(self) toolbar_box.toolbar.insert(self.activity_button, 0) self.activity_button.show() title_entry = TitleEntry(self) toolbar_box.toolbar.insert(title_entry, -1) title_entry.show() description_item = DescriptionItem(self) toolbar_box.toolbar.insert(description_item, -1) description_item.show() self._share_button = ShareButton(self) toolbar_box.toolbar.insert(self._share_button, -1) self._share_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 following an invite 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._share_button.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 __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) 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_button = ActivityButton(self) toolbar_box.toolbar.insert(self.activity_button, 0) self.activity_button.show() title_entry = TitleEntry(self) toolbar_box.toolbar.insert(title_entry, -1) title_entry.show() description_item = DescriptionItem(self) toolbar_box.toolbar.insert(description_item, -1) description_item.show() self._share_button = ShareButton(self) toolbar_box.toolbar.insert(self._share_button, -1) self._share_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 following an invite 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._share_button.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)
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) 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_button = ActivityButton(self) toolbar_box.toolbar.insert(self.activity_button, 0) self.activity_button.show() title_entry = TitleEntry(self) toolbar_box.toolbar.insert(title_entry, -1) title_entry.show() description_item = DescriptionItem(self) toolbar_box.toolbar.insert(description_item, -1) description_item.show() self._share_button = ShareButton(self) toolbar_box.toolbar.insert(self._share_button, -1) self._share_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 following an invite 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._share_button.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 _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] 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 = TelepathyGLib.Connection.new( TelepathyGLib.DBusDaemon.dup(), bus_name, connection) self._one_to_one_connection_ready_cb( TelepathyGLib.DBusDaemon.dup(), 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 = TelepathyGLib.Channel(conn, 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 _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.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()