diff options
author | Aneesh Dogra <lionaneesh@gmail.com> | 2012-12-12 22:18:15 (GMT) |
---|---|---|
committer | Aneesh Dogra <lionaneesh@gmail.com> | 2012-12-12 22:19:56 (GMT) |
commit | d405452a918f863351c12a59df454b1d50fabc98 (patch) | |
tree | d9855d0292120364698418991037e74d29a52ff2 | |
parent | c91b738d8e5016d344ebeab9ea6a9479eac3642a (diff) |
Add Gtk3 version of MiniChat.
Still Lacks :-
1) CSS styles.
2) Scrollbar
-rw-r--r-- | MiniChat_gtk3/MANIFEST | 10 | ||||
-rw-r--r-- | MiniChat_gtk3/activity/activity.info | 8 | ||||
-rw-r--r-- | MiniChat_gtk3/activity/chat.svg | 6 | ||||
-rw-r--r-- | MiniChat_gtk3/minichat.py | 281 | ||||
-rw-r--r-- | MiniChat_gtk3/out | 0 | ||||
-rw-r--r-- | MiniChat_gtk3/po/MiniChat.pot | 50 | ||||
-rwxr-xr-x | MiniChat_gtk3/setup.py | 23 | ||||
-rw-r--r-- | MiniChat_gtk3/textchannel.py | 137 |
8 files changed, 515 insertions, 0 deletions
diff --git a/MiniChat_gtk3/MANIFEST b/MiniChat_gtk3/MANIFEST new file mode 100644 index 0000000..51c6abf --- /dev/null +++ b/MiniChat_gtk3/MANIFEST @@ -0,0 +1,10 @@ + +setup.py + + + +activity/activity.info +minichat.py +textchannel.py +activity/chat.svg +po/MiniChat.pot diff --git a/MiniChat_gtk3/activity/activity.info b/MiniChat_gtk3/activity/activity.info new file mode 100644 index 0000000..4622799 --- /dev/null +++ b/MiniChat_gtk3/activity/activity.info @@ -0,0 +1,8 @@ +[Activity] +name = Mini Chat +bundle_id = net.flossmanuals.MiniChat +icon = chat +exec = sugar-activity minichat.MiniChat +show_launcher = yes +activity_version = 1 +license = GPLv2+ diff --git a/MiniChat_gtk3/activity/chat.svg b/MiniChat_gtk3/activity/chat.svg new file mode 100644 index 0000000..b0df51b --- /dev/null +++ b/MiniChat_gtk3/activity/chat.svg @@ -0,0 +1,6 @@ +<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [ + <!ENTITY stroke_color "#010101"> + <!ENTITY fill_color "#FFFFFF"> +]><svg enable-background="new 0 0 55 55" height="55px" version="1.1" viewBox="0 0 55 55" width="55px" x="0px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" y="0px"><g display="block" id="activity-chat"> + <path d="M9.263,48.396c0.682,1.152,6.027,0.059,8.246-1.463 c2.102-1.432,3.207-2.596,4.336-2.596c1.133,0,12.54,0.92,20.935-5.715c7.225-5.707,9.773-13.788,4.52-21.437 c-5.252-7.644-13.832-9.08-20.878-8.56C16.806,9.342,4.224,16.91,4.677,28.313c0.264,6.711,3.357,9.143,4.922,10.703 c1.562,1.566,4.545,1.566,2.992,5.588C11.981,46.183,8.753,47.522,9.263,48.396z" display="inline" fill="&fill_color;" stroke="&stroke_color;" stroke-width="3.5"/> +</g></svg><!-- " --> diff --git a/MiniChat_gtk3/minichat.py b/MiniChat_gtk3/minichat.py new file mode 100644 index 0000000..6238ccc --- /dev/null +++ b/MiniChat_gtk3/minichat.py @@ -0,0 +1,281 @@ +# minichat.py +# Copyright 2007-2008 One Laptop Per Child +# Copyright 2012 Aneesh Dogra <lionaneesh@gmail.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gi.repository import GObject +from gettext import gettext as _ +from gi.repository import Gtk +from gi.repository import Pango +import logging +from sugar3.activity.activity import Activity, SCOPE_PRIVATE +from sugar3.activity.widgets import ActivityToolbar, StopButton +from sugar3.graphics.alert import NotifyAlert +from sugar3.presence.presenceservice import PresenceService +from sugar3.graphics.style import (Color, COLOR_BLACK, COLOR_WHITE, + COLOR_BUTTON_GREY, FONT_BOLD, FONT_NORMAL) +from sugar3.graphics.xocolor import XoColor +from sugar3.graphics.palette import Palette + +from textchannel import TextChannelWrapper + +logger = logging.getLogger('minichat-activity') + +class MiniChat(Activity): + def __init__(self, handle): + Activity.__init__(self, handle) + + toolbox = ActivityToolbar(self) + + stop_button = StopButton(self) + stop_button.show() + toolbox.insert(stop_button, -1) + + self.set_toolbar_box(toolbox) + toolbox.show() + + self.scroller = Gtk.ScrolledWindow() + self.scroller.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) + + root = self.make_root() + self.set_canvas(root) + root.show_all() + self.entry.grab_focus() + + self.pservice = PresenceService() + self.owner = self.pservice.get_owner() + + # Track last message, to combine several messages: + self._last_msg = None + self._last_msg_sender = None + self.text_channel = None + + if self.shared_activity: + # we are joining the activity + self.connect('joined', self._joined_cb) + if self.get_shared(): + # we have already joined + self._joined_cb() + else: + # we are creating the activity + if not self.metadata or self.metadata.get('share-scope', + SCOPE_PRIVATE) == SCOPE_PRIVATE: + # if we are in private session + self._alert(_('Off-line'), _('Share, or invite someone.')) + self.connect('shared', self._shared_cb) + + def _shared_cb(self, activity): + logger.debug('Chat was shared') + self._setup() + + def _joined_cb(self, activity): + """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 _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.entry.set_sensitive(True) + self.entry.grab_focus() + + def _received_cb(self, buddy, text): + """Show message that was received.""" + if buddy: + nick = buddy.nick + else: + nick = '???' + logger.debug('Received message from %s: %s', nick, text) + self.add_text(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() + + def _alert_cancel_cb(self, alert, response_id): + self.remove_alert(alert) + + def _buddy_joined_cb (self, activity, buddy): + """Show a buddy who joined""" + if buddy == self.owner: + return + if buddy: + nick = buddy.nick + else: + nick = '???' + self.add_text(buddy, buddy.nick+' '+_('joined the chat'), + status_message=True) + + def _buddy_left_cb (self, activity, buddy): + """Show a buddy who joined""" + if buddy == self.owner: + return + if buddy: + nick = buddy.nick + else: + nick = '???' + self.add_text(buddy, buddy.nick+' '+_('left the chat'), + status_message=True) + + def _buddy_already_exists(self, buddy): + """Show a buddy already in the chat.""" + if buddy == self.owner: + return + if buddy: + nick = buddy.nick + else: + nick = '???' + self.add_text(buddy, buddy.nick+' '+_('is here'), + status_message=True) + + def make_root(self): + vbox = Gtk.VBox() + + self.conversation = Gtk.VBox() + self.conversation.show_all() + vbox.add(self.conversation) + + self.entry = Gtk.Entry() + self.entry.modify_bg(Gtk.StateType.INSENSITIVE, + COLOR_WHITE.get_gdk_color()) + self.entry.modify_base(Gtk.StateType.INSENSITIVE, + COLOR_WHITE.get_gdk_color()) + self.entry.set_sensitive(False) + + setattr(self.entry, "nick", "???") + self.entry.connect('activate', self.entry_activate_cb) + vbox.add(self.entry) + + box = Gtk.VBox(homogeneous=False) + box.pack_end(vbox, False, True, 0) + box.show_all() + + return box + + def add_text(self, buddy, text, status_message=False): + """Display text on screen, with name and colors. + + buddy -- buddy object + text -- string, what the buddy said + status_message -- boolean + False: show what buddy said + True: show what buddy did + + Gtk layout: + + .------------- rb ---------------. + | +name_vbox+ +----msg_vbox----+ | + | | | | | | + | | nick: | | +----entry---+ | | + | | | | | text | | | + | +---------+ | +------------+ | | + | | | | + | | +----entry---+ | | + | | | text | | | + | | +------------+ | | + | +----------------+ | + `--------------------------------' + + """ + if buddy: + nick = buddy.props.nick + else: + nick = '???' # XXX: should be '' but leave for debugging + + logger.debug('Nick: %s' % nick) + + # Check for Right-To-Left languages: + if Pango.find_base_dir(nick, -1) == Pango.Direction.RTL: + lang_rtl = True + else: + lang_rtl = False + + logger.debug('lang_rtl: %s' % str(lang_rtl)) + + # Check if new message box or add text to previous: + new_msg = True + if self._last_msg_sender: + if not status_message: + if buddy == self._last_msg_sender: + # Add text to previous message + new_msg = False + + if not new_msg: + rb = self._last_msg + msg_vbox = rb.get_children()[1] + + else: # Its a new_msg, we need to create a new rb + rb = Gtk.HBox() + logger.debug('rb: %s' % str(rb)) + self._last_msg = rb + self._last_msg_sender = buddy + if not status_message: + name = Gtk.Entry() + name.set_text(nick+': ') + name_vbox = Gtk.VBox() + name_vbox.add(name) + rb.pack_start(name_vbox, False, True, 0) + + msg_vbox = Gtk.VBox() + rb.pack_start(msg_vbox, True, True, 0) + + if status_message: + self._last_msg_sender = None + + if text: + msg = Gtk.TextView() + text_buffer = msg.get_buffer() + text_buffer.set_text(text) + msg.show() + msg.set_editable(False) + msg.set_justification(Gtk.Justification.LEFT) + msg.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) + msg_vbox.pack_start(msg, True, True, 2) + + # Order of boxes for RTL languages: + if lang_rtl: + msg_hbox.reverse() + if new_msg: + rb.reverse() + + if new_msg: + self.conversation.add(rb) + rb.show_all() + + def entry_activate_cb(self, entry): + text = entry.get_text() + logger.debug('Entry: %s' % text) + if text: + self.add_text(self.owner, text) + entry.set_text('') + if self.text_channel: + self.text_channel.send(text) + else: + logger.debug('Tried to send message but text channel ' + 'not connected.') diff --git a/MiniChat_gtk3/out b/MiniChat_gtk3/out new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/MiniChat_gtk3/out diff --git a/MiniChat_gtk3/po/MiniChat.pot b/MiniChat_gtk3/po/MiniChat.pot new file mode 100644 index 0000000..71af33d --- /dev/null +++ b/MiniChat_gtk3/po/MiniChat.pot @@ -0,0 +1,50 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-12-13 03:49+0530\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: activity/activity.info:2 +msgid "Mini Chat" +msgstr "" + +#: minichat.py:77 +msgid "Off-line" +msgstr "" + +#: minichat.py:77 +msgid "Share, or invite someone." +msgstr "" + +#: minichat.py:98 +msgid "On-line" +msgstr "" + +#: minichat.py:98 +msgid "Connected" +msgstr "" + +#: minichat.py:132 +msgid "joined the chat" +msgstr "" + +#: minichat.py:143 +msgid "left the chat" +msgstr "" + +#: minichat.py:154 +msgid "is here" +msgstr "" diff --git a/MiniChat_gtk3/setup.py b/MiniChat_gtk3/setup.py new file mode 100755 index 0000000..601d35c --- /dev/null +++ b/MiniChat_gtk3/setup.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python + +# setup.py + +# Copyright (C) 2006, Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from sugar3.activity import bundlebuilder + +bundlebuilder.start() diff --git a/MiniChat_gtk3/textchannel.py b/MiniChat_gtk3/textchannel.py new file mode 100644 index 0000000..1dfe1ac --- /dev/null +++ b/MiniChat_gtk3/textchannel.py @@ -0,0 +1,137 @@ +# textchannel.py + +# Copyright 2007-2008 One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging + +from telepathy.client import Connection, Channel +from telepathy.interfaces import ( + CHANNEL_INTERFACE, CHANNEL_INTERFACE_GROUP, CHANNEL_TYPE_TEXT, + CONN_INTERFACE_ALIASING) +from telepathy.constants import ( + CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES, + CHANNEL_TEXT_MESSAGE_TYPE_NORMAL) + +class TextChannelWrapper(object): + """Wrap a telepathy Text Channel to make usage simpler.""" + def __init__(self, text_chan, conn): + """Connect to the text channel""" + self._activity_cb = None + self._activity_close_cb = None + self._text_chan = text_chan + self._conn = conn + self._logger = logging.getLogger( + 'minichat-activity.TextChannelWrapper') + self._signal_matches = [] + m = self._text_chan[CHANNEL_INTERFACE].connect_to_signal( + 'Closed', self._closed_cb) + self._signal_matches.append(m) + + def send(self, text): + """Send text over the Telepathy text channel.""" + # XXX Implement CHANNEL_TEXT_MESSAGE_TYPE_ACTION + if self._text_chan is not None: + self._text_chan[CHANNEL_TYPE_TEXT].Send( + CHANNEL_TEXT_MESSAGE_TYPE_NORMAL, text) + + def close(self): + """Close the text channel.""" + self._logger.debug('Closing text channel') + try: + self._text_chan[CHANNEL_INTERFACE].Close() + except: + self._logger.debug('Channel disappeared!') + self._closed_cb() + + def _closed_cb(self): + """Clean up text channel.""" + self._logger.debug('Text channel closed.') + for match in self._signal_matches: + match.remove() + self._signal_matches = [] + self._text_chan = None + if self._activity_close_cb is not None: + self._activity_close_cb() + + def set_received_callback(self, callback): + """Connect the function callback to the signal. + + callback -- callback function taking buddy and text args + """ + if self._text_chan is None: + return + self._activity_cb = callback + m = self._text_chan[CHANNEL_TYPE_TEXT].connect_to_signal('Received', + self._received_cb) + self._signal_matches.append(m) + + def handle_pending_messages(self): + """Get pending messages and show them as received.""" + for id, timestamp, sender, type, flags, text in \ + self._text_chan[ + CHANNEL_TYPE_TEXT].ListPendingMessages(False): + self._received_cb(id, timestamp, sender, type, flags, text) + + def _received_cb(self, id, timestamp, sender, type, flags, text): + """Handle received text from the text channel. + + Converts sender to a Buddy. + Calls self._activity_cb which is a callback to the activity. + """ + if self._activity_cb: + buddy = self._get_buddy(sender) + self._activity_cb(buddy, text) + self._text_chan[ + CHANNEL_TYPE_TEXT].AcknowledgePendingMessages([id]) + else: + self._logger.debug('Throwing received message on the floor' + ' since there is no callback connected. See ' + 'set_received_callback') + + def set_closed_callback(self, callback): + """Connect a callback for when the text channel is closed. + + callback -- callback function taking no args + + """ + self._activity_close_cb = callback + + def _get_buddy(self, cs_handle): + """Get a Buddy from a (possibly channel-specific) handle.""" + # XXX This will be made redundant once Presence Service + # provides buddy resolution + from sugar.presence import presenceservice + # Get the Presence Service + pservice = presenceservice.get_instance() + # Get the Telepathy Connection + tp_name, tp_path = pservice.get_preferred_connection() + conn = Connection(tp_name, tp_path) + group = self._text_chan[CHANNEL_INTERFACE_GROUP] + my_csh = group.GetSelfHandle() + if my_csh == cs_handle: + handle = conn.GetSelfHandle() + elif group.GetGroupFlags() & \ + CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES: + handle = group.GetHandleOwners([cs_handle])[0] + else: + handle = cs_handle + + # XXX: deal with failure to get the handle owner + assert handle != 0 + + return pservice.get_buddy_by_telepathy_handle( + tp_name, tp_path, handle) |