Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/pippy_app.py
diff options
context:
space:
mode:
authorC. Scott Ananian <cscott@laptop.org>2007-12-19 22:44:10 (GMT)
committer Morgan Collett <morgan.collett@gmail.com>2007-12-21 14:28:51 (GMT)
commitb93e199e7f0d5127be93c5c89599bd74e84e39bc (patch)
tree6182c0a4616f7595b6fdb90d13d64fe54a20aef6 /pippy_app.py
parent87bd10b7710e1d0b6c31eeedc5509a7626c43a93 (diff)
Rename files to match standard "Pippy application" layout.
Diffstat (limited to 'pippy_app.py')
-rw-r--r--pippy_app.py482
1 files changed, 482 insertions, 0 deletions
diff --git a/pippy_app.py b/pippy_app.py
new file mode 100644
index 0000000..57d560a
--- /dev/null
+++ b/pippy_app.py
@@ -0,0 +1,482 @@
+# Copyright 2007 Collabora Ltd.
+#
+# 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 gettext import gettext as _
+import dbus
+import hippo
+import gtk
+import pango
+import logging
+import re
+from datetime import datetime
+
+from sugar import profile
+from sugar.activity.activity import Activity, ActivityToolbox
+from sugar.graphics.alert import NotifyAlert
+from sugar.graphics.style import (Color, COLOR_BLACK, COLOR_WHITE,
+ FONT_BOLD, FONT_NORMAL)
+from sugar.graphics.roundbox import CanvasRoundBox
+from sugar.graphics.xocolor import XoColor
+from sugar.graphics.palette import Palette, CanvasInvoker
+from sugar.graphics.menuitem import MenuItem
+from sugar.presence import presenceservice
+
+from telepathy.client import Connection, Channel
+
+from telepathy.interfaces import (
+ CONN_INTERFACE, PROPERTIES_INTERFACE,
+ CHANNEL_INTERFACE_GROUP, CONN_INTERFACE_ALIASING,
+ CHANNEL_TYPE_TEXT)
+
+from telepathy.constants import (
+ CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES,
+ CONNECTION_HANDLE_TYPE_NONE, CONNECTION_HANDLE_TYPE_CONTACT,
+ CONNECTION_HANDLE_TYPE_ROOM, CHANNEL_TEXT_MESSAGE_TYPE_NORMAL,
+ CONNECTION_STATUS_CONNECTED, CONNECTION_STATUS_DISCONNECTED,
+ CONNECTION_STATUS_CONNECTING)
+
+logger = logging.getLogger('chat-activity')
+
+class Chat(Activity):
+ def __init__(self, handle):
+ Activity.__init__(self, handle)
+
+ root = self.make_root()
+ self.set_canvas(root)
+ root.show_all()
+ self.entry.grab_focus()
+
+ toolbox = ActivityToolbox(self)
+ self.set_toolbox(toolbox)
+ toolbox.show()
+
+ self.owner = self._pservice.get_owner()
+ self._chat_log = ''
+ # Auto vs manual scrolling:
+ self._scroll_auto = True
+ self._scroll_value = 0.0
+
+ self.connect('shared', self._shared_cb)
+ 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
+ self._alert(_('Off-line'), _('Share, or invite someone.'))
+
+ def _shared_cb(self, activity):
+ logger.debug('Chat was shared')
+ self._setup()
+
+ def _setup(self):
+ self.text_channel = TextChannelWrapper(self)
+ 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_editable(True)
+
+ 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 _received_cb(self, buddy, text):
+ """Show message that was received."""
+ if buddy:
+ nick = buddy.props.nick
+ else:
+ nick = '???'
+ 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.props.nick
+ else:
+ nick = '???'
+ self.add_text(buddy, buddy.props.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.props.nick
+ else:
+ nick = '???'
+ self.add_text(buddy, buddy.props.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.props.nick
+ else:
+ nick = '???'
+ self.add_text(buddy, buddy.props.nick+' '+_('is here'),
+ status_message=True)
+
+ def make_root(self):
+ conversation = hippo.CanvasBox(
+ spacing=4,
+ background_color=COLOR_WHITE.get_int())
+ self.conversation = conversation
+
+ entry = gtk.Entry()
+ entry.set_editable(False)
+ entry.connect('activate', self.entry_activate_cb)
+ self.entry = entry
+
+ hbox = gtk.HBox()
+ hbox.add(entry)
+
+ canvas = hippo.Canvas()
+ canvas.set_root(conversation)
+
+ sw = gtk.ScrolledWindow()
+ sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
+ sw.add_with_viewport(canvas)
+ self.scrolled_window = sw
+
+ vadj = self.scrolled_window.get_vadjustment()
+ vadj.connect('changed', self.rescroll)
+ vadj.connect('value-changed', self.scroll_value_changed_cb)
+
+ widget = hippo.CanvasWidget(widget=sw)
+
+ box = gtk.VBox(homogeneous=False)
+ box.pack_start(sw)
+ box.pack_start(hbox, expand=False)
+
+ return box
+
+ def rescroll(self, adj, scroll=None):
+ """Scroll the chat window to the bottom"""
+ if self._scroll_auto:
+ adj.set_value(adj.upper-adj.page_size)
+ self._scroll_value = adj.get_value()
+
+ def scroll_value_changed_cb(self, adj, scroll=None):
+ """Turn auto scrolling on or off.
+
+ If the user scrolled up, turn it off.
+ If the user scrolled to the bottom, turn it back on.
+ """
+ if adj.get_value() < self._scroll_value:
+ self._scroll_auto = False
+ elif adj.get_value() == adj.upper-adj.page_size:
+ self._scroll_auto = True
+
+ def add_text(self, buddy, text, status_message=False):
+ """Display text on screen, with name and colors.
+
+ buddy -- buddy object or dict {nick: string, color: string}
+ text -- string, what the buddy said
+ status_message -- boolean
+ False: show what buddy said
+ True: show what buddy did
+ """
+ if buddy:
+ if type(buddy) is dict:
+ nick = buddy['nick']
+ color = buddy['color']
+ else:
+ nick = buddy.props.nick
+ color = buddy.props.color
+ try:
+ color_stroke, color_fill = color.split(',')
+ except ValueError:
+ color_stroke, color_fill = ('#000000', '#888888')
+ color_stroke = Color(color_stroke).get_int()
+ color_fill = Color(color_fill).get_int()
+ text_color = COLOR_WHITE.get_int()
+ self._add_log(nick, color, text, status_message)
+ else:
+ nick = '???' # XXX: should be '' but leave for debugging
+ color_stroke = COLOR_BLACK.get_int()
+ color_fill = COLOR_WHITE.get_int()
+ text_color = COLOR_BLACK.get_int()
+
+ rb = CanvasRoundBox(background_color=color_fill,
+ border_color=color_stroke,
+ padding=4)
+ rb.props.border_color = color_stroke # Bug #3742
+
+ if not status_message:
+ name = hippo.CanvasText(text=nick+': ',
+ color=text_color,
+ font_desc=FONT_BOLD.get_pango_desc())
+ rb.append(name)
+
+ regexp = re.compile('((http|ftp)s?://)?'
+ '(([-a-zA-Z0-9]+[.])+[-a-zA-Z0-9]{2,}|([0-9]{1,3}[.]){3}[0-9]{1,3})'
+ '(:[1-9][0-9]{0,4})?(/[-a-zA-Z0-9/%~@&_+=;:,.?#]*[a-zA-Z0-9/])?')
+ match = regexp.search(text)
+ while match:
+ # there is a URL in the text
+ starttext = text[:match.start()]
+ if starttext:
+ message = hippo.CanvasText(
+ text=starttext,
+ size_mode=hippo.CANVAS_SIZE_WRAP_WORD,
+ color=text_color,
+ font_desc=FONT_NORMAL.get_pango_desc(),
+ xalign=hippo.ALIGNMENT_START)
+ rb.append(message)
+ url = text[match.start():match.end()]
+ message = hippo.CanvasLink(
+ text=url,
+ color=text_color,
+ font_desc=FONT_BOLD.get_pango_desc(),
+ )
+ attrs = pango.AttrList()
+ attrs.insert(pango.AttrUnderline(pango.UNDERLINE_SINGLE, 0, 32767))
+ message.set_property("attributes", attrs)
+
+ palette = URLMenu(url)
+ palette.props.invoker = CanvasInvoker(message)
+
+ rb.append(message)
+ text = text[match.end():]
+ match = regexp.search(text)
+ if text:
+ message = hippo.CanvasText(
+ text=text,
+ size_mode=hippo.CANVAS_SIZE_WRAP_WORD,
+ color=text_color,
+ font_desc=FONT_NORMAL.get_pango_desc(),
+ xalign=hippo.ALIGNMENT_START)
+ rb.append(message)
+
+ box = hippo.CanvasBox(padding=4)
+ box.append(rb)
+ self.conversation.append(box)
+
+ def entry_activate_cb(self, entry):
+ text = entry.props.text
+ logger.debug('Entry: %s' % text)
+ if text:
+ self.add_text(self.owner, text)
+ entry.props.text = ''
+ if self.text_channel:
+ self.text_channel.send(text)
+ else:
+ logger.debug('Tried to send message but text channel '
+ 'not connected.')
+
+ def _add_log(self, nick, color, text, status_message):
+ """Add the text to the chat log.
+
+ nick -- string, buddy nickname
+ color -- string, buddy.props.color
+ text -- string, body of message
+ status_message -- boolean
+ """
+ self._chat_log += '%s\t%s\t%s\t%d\t%s\n' % (
+ datetime.strftime(datetime.now(), '%b %d %H:%M:%S'),
+ nick, color, status_message, text)
+
+ def _get_log(self):
+ return self._chat_log
+
+ 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)
+ f = open(file_path, 'w')
+ try:
+ f.write(self._get_log())
+ finally:
+ f.close()
+
+ 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()
+ for line in log:
+ timestamp, nick, color, status, text = line.strip().split('\t')
+ status_message = bool(int(status))
+ self.add_text({'nick': nick, 'color': color},
+ text, status_message)
+
+
+class TextChannelWrapper(object):
+ """Wrap a telepathy text Channel to make usage simpler."""
+ def __init__(self, activity):
+ """Connect to the text channel if possible."""
+ self._text_chan = None
+ self._activity_cb = None
+ self._activity = activity
+ self._logger = logging.getLogger(
+ 'chat-activity.TextChannelWrapper')
+ self._connect()
+
+ def _connect(self):
+ bus_name, conn_path, channel_paths =\
+ self._activity._shared_activity.get_channels()
+ for channel_path in channel_paths:
+ channel = Channel(bus_name, channel_path)
+ htype, handle = channel.GetHandle()
+ if htype == CONNECTION_HANDLE_TYPE_ROOM:
+ self._logger.debug(
+ 'Found our room: it has handle#%d' % handle)
+ room = handle
+ ctype = channel.GetChannelType()
+ if ctype == CHANNEL_TYPE_TEXT:
+ self._logger.debug(
+ 'Found our Text channel at %s' % channel_path)
+ self._text_chan = channel
+
+ def connected(self):
+ return (self._text_chan is not None)
+
+ def send(self, text):
+ if self._text_chan:
+ self._text_chan[CHANNEL_TYPE_TEXT].Send(
+ CHANNEL_TEXT_MESSAGE_TYPE_NORMAL, text)
+
+ def close(self):
+ if self._text_chan:
+ self._text_chan.Close()
+ self._text_chan = None
+
+ def set_received_callback(self, callback):
+ """Connect the function callback to the signal.
+
+ callback -- callback function taking buddy and text args
+ """
+ if not self._text_chan:
+ self._logger.debug(
+ 'Failed to connect callback - text channel not connected.')
+ return
+ self._activity_cb = callback
+ self._text_chan[CHANNEL_TYPE_TEXT].connect_to_signal('Received',
+ self._received_cb)
+
+ 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:
+ # XXX: cache these
+ buddy = self._get_buddy(sender)
+ self._activity_cb(buddy, text)
+ else:
+ self._logger.debug('Throwing received message on the floor'
+ ' since there is no callback connected. See '
+ 'set_received_callback')
+
+ def _get_buddy(self, cs_handle):
+ """Get a Buddy from a handle."""
+ tp_name, tp_path =\
+ self._activity._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 self._activity._pservice.get_buddy_by_telepathy_handle(
+ tp_name, tp_path, handle)
+
+class URLMenu(Palette):
+ def __init__(self, url):
+ Palette.__init__(self, url)
+
+ protocols = ['http://', 'https://', 'ftp://', 'ftps://']
+ no_protocol = True
+ for protocol in protocols:
+ if url.startswith(protocol):
+ no_protocol = False
+ if no_protocol:
+ url = 'http://' + url
+ self.url = url
+
+ menu_item = MenuItem(_('Copy to Clipboard'), 'edit-copy')
+ menu_item.connect('activate', self._copy_to_clipboard_cb)
+ self.menu.append(menu_item)
+ menu_item.show()
+
+ def _copy_to_clipboard_cb(self, menuitem):
+ logger.debug('Copy %s to clipboard', self.url)
+ clipboard = gtk.clipboard_get()
+ targets = [("text/uri-list", 0, 0)]
+
+ if not clipboard.set_with_data(targets,
+ self._clipboard_data_get_cb,
+ self._clipboard_clear_cb,
+ (self.url)):
+ logger.error('GtkClipboard.set_with_data failed!')
+ else:
+ self.owns_clipboard = True
+
+ def _clipboard_data_get_cb(self, clipboard, selection, info, data):
+ logger.debug('_clipboard_data_get_cb data=%s target=%s', data,
+ selection.target)
+ if selection.target in ['text/uri-list']:
+ if not selection.set_uris([data]):
+ logger.debug('failed to set_uris')
+ else:
+ logger.debug('not uri')
+ if not selection.set_text(data):
+ logger.debug('failed to set_text')
+
+ def _clipboard_clear_cb(self, clipboard, data):
+ logger.debug('clipboard_clear_cb')
+ self.owns_clipboard = False