Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/chat
diff options
context:
space:
mode:
authorAleksey Lim <alsroot@member.fsf.org>2010-12-29 14:45:11 (GMT)
committer Aleksey Lim <alsroot@member.fsf.org>2010-12-29 14:45:11 (GMT)
commite7d2bc3705341aa8d6c1a807d88c86b408459bb5 (patch)
tree09e787eab03508421abe04e583c3370bebeb737b /chat
parentfa12d299101da4b5a25bdfe591411f6985fcb7c1 (diff)
Cleanup to code
- sugar-lint fixes - move chatbox code to separate modules to reuse in other activities - more robust smiley parsing - remove pippy code
Diffstat (limited to 'chat')
-rw-r--r--chat/__init__.py0
-rw-r--r--chat/box.py398
-rw-r--r--chat/smilies.py152
3 files changed, 550 insertions, 0 deletions
diff --git a/chat/__init__.py b/chat/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/chat/__init__.py
diff --git a/chat/box.py b/chat/box.py
new file mode 100644
index 0000000..d201b20
--- /dev/null
+++ b/chat/box.py
@@ -0,0 +1,398 @@
+# Copyright 2007-2008 One Laptop Per Child
+# Copyright 2009, Aleksey Lim
+# Copyright 2010, Mukesh Gupta
+#
+# 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 re
+import os
+import time
+import logging
+from datetime import datetime
+from gettext import gettext as _
+from os.path import join
+
+import gtk
+import hippo
+import pango
+import cairo
+
+from sugar.graphics import style
+from sugar.graphics.roundbox import CanvasRoundBox
+from sugar.graphics.palette import Palette, CanvasInvoker
+from sugar.presence import presenceservice
+from sugar.graphics.menuitem import MenuItem
+from sugar.activity.activity import get_activity_root, show_object_in_journal
+from sugar.util import timestamp_to_elapsed_string
+from sugar.datastore import datastore
+from sugar import profile
+
+from chat import smilies
+
+
+_URL_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/])?')
+
+
+class ChatBox(hippo.CanvasScrollbars):
+
+ def __init__(self):
+ hippo.CanvasScrollbars.__init__(self)
+
+ self.owner = presenceservice.get_instance().get_owner()
+
+ # Auto vs manual scrolling:
+ self._scroll_auto = True
+ self._scroll_value = 0.0
+ self._last_msg_sender = None
+ # Track last message, to combine several messages:
+ self._last_msg = None
+ self._chat_log = ''
+
+ self._conversation = hippo.CanvasBox(
+ spacing=0,
+ background_color=style.COLOR_WHITE.get_int())
+
+ self.set_policy(hippo.ORIENTATION_HORIZONTAL,
+ hippo.SCROLLBAR_NEVER)
+ self.set_root(self._conversation)
+
+ vadj = self.props.widget.get_vadjustment()
+ vadj.connect('changed', self._scroll_changed_cb)
+ vadj.connect('value-changed', self._scroll_value_changed_cb)
+
+ def get_log(self):
+ return self._chat_log
+
+ 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}
+ (The dict is for loading the chat log from the journal,
+ when we don't have the buddy object any more.)
+ text -- string, what the buddy said
+ status_message -- boolean
+ False: show what buddy said
+ True: show what buddy did
+
+ hippo layout:
+ .------------- rb ---------------.
+ | +name_vbox+ +----msg_vbox----+ |
+ | | | | | |
+ | | nick: | | +--msg_hbox--+ | |
+ | | | | | text | | |
+ | +---------+ | +------------+ | |
+ | | | |
+ | | +--msg_hbox--+ | |
+ | | | text | url | | |
+ | | +------------+ | |
+ | +----------------+ |
+ `--------------------------------'
+ """
+ if not buddy:
+ buddy = self.owner
+
+ if type(buddy) is dict:
+ # dict required for loading chat log from journal
+ nick = buddy['nick']
+ color = buddy['color']
+ else:
+ nick = buddy.props.nick
+ color = buddy.props.color
+ try:
+ color_stroke_html, color_fill_html = color.split(',')
+ except ValueError:
+ color_stroke_html, color_fill_html = ('#000000', '#888888')
+
+ # Select text color based on fill color:
+ color_fill_rgba = style.Color(color_fill_html).get_rgba()
+ color_fill_gray = (color_fill_rgba[0] + color_fill_rgba[1] +
+ color_fill_rgba[2]) / 3
+ color_stroke = style.Color(color_stroke_html).get_int()
+ color_fill = style.Color(color_fill_html).get_int()
+
+ if color_fill_gray < 0.5:
+ text_color = style.COLOR_WHITE.get_int()
+ else:
+ text_color = style.COLOR_BLACK.get_int()
+
+ self._add_log(nick, color, text, status_message)
+
+ # Check for Right-To-Left languages:
+ if pango.find_base_dir(nick, -1) == pango.DIRECTION_RTL:
+ lang_rtl = True
+ else:
+ lang_rtl = False
+
+ # 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]
+ msg_hbox = hippo.CanvasBox(
+ orientation=hippo.ORIENTATION_HORIZONTAL)
+ msg_vbox.append(msg_hbox)
+ else:
+ rb = CanvasRoundBox(background_color=color_fill,
+ border_color=color_stroke,
+ padding=4)
+ rb.props.border_color = color_stroke # Bug #3742
+ self._last_msg = rb
+ self._last_msg_sender = buddy
+ if not status_message:
+ name = hippo.CanvasText(text=nick + ': ', color=text_color)
+ name_vbox = hippo.CanvasBox(
+ orientation=hippo.ORIENTATION_VERTICAL)
+ name_vbox.append(name)
+ rb.append(name_vbox)
+ msg_vbox = hippo.CanvasBox(
+ orientation=hippo.ORIENTATION_VERTICAL)
+ rb.append(msg_vbox)
+ msg_hbox = hippo.CanvasBox(
+ orientation=hippo.ORIENTATION_HORIZONTAL)
+ msg_vbox.append(msg_hbox)
+
+ if status_message:
+ self._last_msg_sender = None
+
+ match = _URL_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,
+ xalign=hippo.ALIGNMENT_START)
+ msg_hbox.append(message)
+ url = text[match.start():match.end()]
+
+ message = _CanvasLink(
+ text=url,
+ color=text_color)
+ attrs = pango.AttrList()
+ attrs.insert(pango.AttrUnderline(pango.UNDERLINE_SINGLE, 0, 32767))
+ message.set_property("attributes", attrs)
+ message.connect('activated', self._link_activated_cb)
+
+ # call interior magic which should mean just:
+ # CanvasInvoker().parent = message
+ CanvasInvoker(message)
+
+ msg_hbox.append(message)
+ text = text[match.end():]
+ match = _URL_REGEXP.search(text)
+
+ if text:
+ for word in smilies.parse(text):
+ if isinstance(word, cairo.ImageSurface):
+ item = hippo.CanvasImage(
+ image=word,
+ border=0,
+ border_color=style.COLOR_BUTTON_GREY.get_int(),
+ xalign=hippo.ALIGNMENT_CENTER,
+ yalign=hippo.ALIGNMENT_CENTER)
+ else:
+ item = hippo.CanvasText(
+ text=word,
+ size_mode=hippo.CANVAS_SIZE_WRAP_WORD,
+ color=text_color,
+ xalign=hippo.ALIGNMENT_START)
+ msg_hbox.append(item)
+
+ # Order of boxes for RTL languages:
+ if lang_rtl:
+ msg_hbox.reverse()
+ if new_msg:
+ rb.reverse()
+
+ if new_msg:
+ box = hippo.CanvasBox(padding=2)
+ box.append(rb)
+ self._conversation.append(box)
+
+ def add_separator(self, timestamp):
+ """Add whitespace and timestamp between chat sessions."""
+ time_with_current_year = (time.localtime(time.time())[0],) +\
+ time.strptime(timestamp, "%b %d %H:%M:%S")[1:]
+
+ timestamp_seconds = time.mktime(time_with_current_year)
+ if timestamp_seconds > time.time():
+ time_with_previous_year = (time.localtime(time.time())[0]-1,) +\
+ time.strptime(timestamp, "%b %d %H:%M:%S")[1:]
+ timestamp_seconds = time.mktime(time_with_previous_year)
+
+ message = hippo.CanvasText(
+ text=timestamp_to_elapsed_string(timestamp_seconds),
+ color=style.COLOR_BUTTON_GREY.get_int(),
+ font_desc=style.FONT_NORMAL.get_pango_desc(),
+ xalign=hippo.ALIGNMENT_CENTER)
+
+ box = hippo.CanvasBox(padding=2)
+ box.append(message)
+ self._conversation.append(box)
+ self.add_log_timestamp(timestamp)
+
+ self._last_msg_sender = None
+
+ def add_log_timestamp(self, existing_timestamp=None):
+ """Add a timestamp entry to the chat log."""
+ if existing_timestamp is not None:
+ self._chat_log += '%s\t\t\n' % existing_timestamp
+ else:
+ self._chat_log += '%s\t\t\n' % (
+ datetime.strftime(datetime.now(), '%b %d %H:%M:%S'))
+
+ 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
+ """
+ if not nick:
+ nick = '???'
+ if not color:
+ color = '#000000,#FFFFFF'
+ if not text:
+ text = '-'
+ if not status_message:
+ status_message = False
+ 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 _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 _scroll_changed_cb(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 _link_activated_cb(self, link):
+ url = _url_check_protocol(link.props.text)
+ self._show_via_journal(url)
+
+ def _show_via_journal(self, url):
+ """Ask the journal to display a URL"""
+ logging.debug('Create journal entry for URL: %s', url)
+ jobject = datastore.create()
+ metadata = {
+ 'title': "%s: %s" % (_('URL from Chat'), url),
+ 'title_set_by_user': '1',
+ 'icon-color': profile.get_color().to_string(),
+ 'mime_type': 'text/uri-list',
+ }
+ for k, v in metadata.items():
+ jobject.metadata[k] = v
+ file_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)
+
+
+class _CanvasLink(hippo.CanvasLink):
+
+ def __init__(self, **kwargs):
+ hippo.CanvasLink.__init__(self, **kwargs)
+
+ def create_palette(self):
+ return _URLMenu(self.props.text)
+
+
+class _URLMenu(Palette):
+
+ def __init__(self, url):
+ Palette.__init__(self, url)
+
+ self.owns_clipboard = False
+ self.url = _url_check_protocol(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 create_palette(self):
+ pass
+
+ def _copy_to_clipboard_cb(self, menuitem):
+ logging.debug('Copy %s to clipboard', self.url)
+ clipboard = gtk.clipboard_get()
+ targets = [("text/uri-list", 0, 0),
+ ("UTF8_STRING", 0, 1)]
+
+ if not clipboard.set_with_data(targets,
+ self._clipboard_data_get_cb,
+ self._clipboard_clear_cb,
+ (self.url)):
+ logging.error('GtkClipboard.set_with_data failed!')
+ else:
+ self.owns_clipboard = True
+
+ def _clipboard_data_get_cb(self, clipboard, selection, info, data):
+ logging.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]):
+ logging.debug('failed to set_uris')
+ else:
+ logging.debug('not uri')
+ if not selection.set_text(data):
+ logging.debug('failed to set_text')
+
+ def _clipboard_clear_cb(self, clipboard, data):
+ logging.debug('clipboard_clear_cb')
+ self.owns_clipboard = False
+
+
+def _url_check_protocol(url):
+ """Check that the url has a protocol, otherwise prepend https://
+
+ url -- string
+
+ Returns url -- string
+ """
+ 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
+ return url
diff --git a/chat/smilies.py b/chat/smilies.py
new file mode 100644
index 0000000..c3cc244
--- /dev/null
+++ b/chat/smilies.py
@@ -0,0 +1,152 @@
+# Copyright 2010, Mukesh Gupta
+# Copyright 2010, Aleksey Lim
+#
+# 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 os
+from os.path import join, exists
+from gettext import gettext as _
+
+import gtk
+import cairo
+
+from sugar.graphics import style
+from sugar.activity.activity import get_activity_root, get_bundle_path
+
+
+THEME = [
+ ('smile', _('Smile'), [':-)', ':)']),
+ ('wink', _('Winking'), [';-)', ';)']),
+ ('confused', _('Confused'), [':-/', ':/']),
+ ('sad', _('Sad'), [':-(', ':(']),
+ ('grin', _('Grin'), [':-D', ':D']),
+ ('neutral', _('Neutral'), (':-|', ':|')),
+ ('shock', _('Shock'), [':-O', ':O', '=-O', '=O']),
+ ('cool', _('Cool'), ['B-)', 'B)', '8-)', '8)']),
+ ('tongue', _('Tongue'), [':-P', ':P']),
+ ('blush', _('Blushing'), [':">']),
+ ('weep', _('Weeping'), [":'-(", ":'("]),
+ ('angel', _('Angel'), ['O-)', 'O)', 'O:-)', 'O:)']),
+ ('shutup', _("Don't tell anyone"), (':-$', ':-$')),
+ ('angry', _('Angry'), ('x-(', 'x(', 'X-(', 'x-(')),
+ ('devil', _('Devil'), ('>:>', '>:)')),
+ ('nerd', _('Nerd'), (':-B', ':B')),
+ ('kiss', _('Kiss'), (':-*', ':*')),
+ ('laugh', _('Laughing'), [':))']),
+ ('sleep', _('Sleepy'), ['I-)']),
+ ('sick', _('Sick'), [':-&']),
+ ('eyebrow', _('Raised eyebrows'), ['/:)']),
+ ]
+
+SMILIES_SIZE = int(style.STANDARD_ICON_SIZE * 0.75)
+
+_catalog = None
+
+
+def init():
+ """Initialise smilies data."""
+ global _catalog
+
+ if _catalog is not None:
+ return
+ _catalog = {}
+
+ png_dir = join(get_activity_root(), 'data', 'icons', 'smilies')
+ svg_dir = join(get_bundle_path(), 'icons', 'smilies')
+
+ if not exists(png_dir):
+ os.makedirs(png_dir)
+
+ for index, (name, hint, codes) in enumerate(THEME):
+ png_path = join(png_dir, name + '.png')
+
+ for i in codes:
+ _catalog[i] = png_path
+ THEME[index] = (png_path, hint, codes)
+
+ if not exists(png_path):
+ pixbuf = _from_svg_at_size(
+ join(svg_dir, name + '.svg'),
+ SMILIES_SIZE, SMILIES_SIZE, None, True)
+ pixbuf.save(png_path, 'png')
+
+
+def parse(text):
+ """Initialise smilies data.
+
+ :param text:
+ string to parse for smilies
+ :returns:
+ array of string parts and ciaro surfaces
+
+ """
+ result = [text]
+
+ for smiley in sorted(_catalog.keys(), lambda x, y: cmp(len(y), len(x))):
+ smiley_surface = cairo.ImageSurface.create_from_png(_catalog[smiley])
+ new_result = []
+
+ for word in result:
+ if isinstance(word, cairo.ImageSurface):
+ new_result.append(word)
+ else:
+ parts = word.split(smiley)
+ for i in parts[:-1]:
+ new_result.append(i)
+ new_result.append(smiley_surface)
+ new_result.append(parts[-1])
+
+ result = new_result
+
+ return result
+
+
+def _from_svg_at_size(filename=None, width=None, height=None, handle=None,
+ keep_ratio=True):
+ """Scale and load SVG into pixbuf."""
+ import rsvg
+
+ if not handle:
+ handle = rsvg.Handle(filename)
+
+ dimensions = handle.get_dimension_data()
+ icon_width = dimensions[0]
+ icon_height = dimensions[1]
+ if icon_width != width or icon_height != height:
+ ratio_width = float(width) / icon_width
+ ratio_height = float(height) / icon_height
+
+ if keep_ratio:
+ ratio = min(ratio_width, ratio_height)
+ if ratio_width != ratio:
+ ratio_width = ratio
+ width = int(icon_width * ratio)
+ elif ratio_height != ratio:
+ ratio_height = ratio
+ height = int(icon_height * ratio)
+ else:
+ ratio_width = 1
+ ratio_height = 1
+
+ surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
+ context = cairo.Context(surface)
+ context.scale(ratio_width, ratio_height)
+ handle.render_cairo(context)
+
+ loader = gtk.gdk.pixbuf_loader_new_with_mime_type('image/png')
+ surface.write_to_png(loader)
+ loader.close()
+
+ return loader.get_pixbuf()