# 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