From a0a134ecca88b8c5ae3f7c2999e13415d9f181b0 Mon Sep 17 00:00:00 2001 From: Aleksey Lim Date: Mon, 02 Feb 2009 01:03:55 +0000 Subject: Migrate chatbox from Chat --- diff --git a/Speak.activity/chatbox.py b/Speak.activity/chatbox.py new file mode 100644 index 0000000..31b3120 --- /dev/null +++ b/Speak.activity/chatbox.py @@ -0,0 +1,381 @@ +# 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 + +# This code is a stripped down version of the Chat + +import gtk +import hippo +import logging +import pango +import re +from datetime import datetime +from gobject import SIGNAL_RUN_FIRST, TYPE_PYOBJECT +from gettext import gettext as _ + +import sugar.graphics.style as style +from sugar.graphics.roundbox import CanvasRoundBox +from sugar.graphics.palette import Palette, CanvasInvoker +from sugar.presence import presenceservice +from sugar.graphics.style import (Color, COLOR_BLACK, COLOR_WHITE) +from sugar.graphics.menuitem import MenuItem +from sugar.activity.activity import get_activity_root + +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(gtk.VBox): + __gsignals__ = { + 'send_message': (SIGNAL_RUN_FIRST, None, [TYPE_PYOBJECT]) } + + def __init__(self): + gtk.VBox.__init__(self, homogeneous=False) + + 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=COLOR_WHITE.get_int()) + + self._scrolled = hippo.CanvasScrollbars() + self._scrolled.set_policy(hippo.ORIENTATION_HORIZONTAL, + hippo.SCROLLBAR_NEVER) + self._scrolled.set_root(self._conversation) + + vadj = self._scrolled.props.widget.get_vadjustment() + vadj.connect('changed', self._scroll_changed_cb) + vadj.connect('value-changed', self._scroll_value_changed_cb) + + self._entry = gtk.Entry() + self._entry.modify_bg(gtk.STATE_INSENSITIVE, + COLOR_WHITE.get_gdk_color()) + self._entry.modify_base(gtk.STATE_INSENSITIVE, + COLOR_WHITE.get_gdk_color()) + #self._entry.set_sensitive(False) + self._entry.connect('activate', self._entry_activate_cb) + + hbox = gtk.HBox() + hbox.add(self._entry) + + canvas = hippo.Canvas() + canvas.set_root(self._scrolled) + + self.pack_start(canvas) + self.pack_start(hbox, expand=False) + + 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 buddy: + 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 = Color(color_fill_html).get_rgba() + color_fill_gray = (color_fill_rgba[0] + color_fill_rgba[1] + + color_fill_rgba[2])/3 + color_stroke = Color(color_stroke_html).get_int() + color_fill = Color(color_fill_html).get_int() + + if color_fill_gray < 0.5: + text_color = COLOR_WHITE.get_int() + else: + text_color = COLOR_BLACK.get_int() + 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() + color = '#000000,#FFFFFF' + + 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: + message = hippo.CanvasText( + text=text, + size_mode=hippo.CANVAS_SIZE_WRAP_WORD, + color=text_color, + xalign=hippo.ALIGNMENT_START) + msg_hbox.append(message) + + # 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 _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 _entry_activate_cb(self, entry): + text = entry.props.text + logging.debug('Entry: %s' % text) + if not text: return + + self.add_text(self.owner, text) + entry.props.text = '' + self.emit('send_message', text) + + + 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""" + import os + import time + from sugar import profile + from sugar.activity.activity import show_object_in_journal + from sugar.datastore import datastore + 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 = 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 _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) + +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.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 + -- cgit v0.9.1