# Copyright (C) 2009, 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 # This code is a stripped down version of the Chat from gi.repository import Gtk from gi.repository import Gdk from gi.repository import GObject import logging from gi.repository import Pango import re from datetime import datetime from gobject import SIGNAL_RUN_FIRST, TYPE_PYOBJECT from gettext import gettext as _ import sugar3.graphics.style as style from sugar3.graphics.roundbox import CanvasRoundBox from sugar3.graphics.palette import Palette, CanvasInvoker from sugar3.presence import presenceservice from sugar3.graphics.style import (Color, COLOR_BLACK, COLOR_WHITE) from sugar3.graphics.menuitem import MenuItem from sugar3.activity.activity import get_activity_root logger = logging.getLogger('speak') 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.ScrolledWindow): def __init__(self): GObject.GObject.__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 = Gtk.VBox() self.conversation.override_background_color(Gtk.StateType.NORMAL,Gdk.RGBA(*COLOR_WHITE.get_rgba())) self.scroller.set_vexpand(True) self.scroller.add_with_viewport(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 Gtk3 layout: .------------- rb ---------------. | +name_vbox+ +----msg_vbox----+ | | | | | | | | | nick: | | +------------+ | | | | | | | Text | | | | +---------+ | +------------+ | | | +----------------+ | `--------------------------------' """ 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 = 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() 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] else: eb = Gtk.EventBox() eb.override_background_color(Gtk.StateType.NORMAL, color_stroke) rb = Gtk.HBox() rb.override_background_color(Gtk.StateType.NORMAL, color_fill) rb.set_border_width(1) eb.add(rb) self._last_msg = rb self._last_msg_sender = buddy if not status_message: name = Gtk.TextView() text_buffer = name.get_buffer() text_buffer.set_text(nick + ': ') name.override_color(Gtk.StarterType.Normal, text_color) name.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) name_vbox = Gtk.Vbox() name_vbox.add(name) rb.add(name_vbox) msg_vbox = Gtk.Vbox() rb.add(msg_vbox) if status_message: self._last_msg_sender = None match = URL_REGEXP.match(text) while match: # there is a URL in the text starttext = text[:match.start()] if starttext: message = Gtk.TextView() text_buffer = message.get_buffer() text_buffer.set_text(starttext) message.set_editable(False) message.set_justification(Gtk.Justification.LEFT) message.override_color(Gtk.StateType.Normal, text_color) message.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) msg_vbox.pack_start(msg, True, True, 0) 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_vbox.pack_start(message, True, True, 0) text = text[match.end():] match = URL_REGEXP.search(text) if text: message = Gtk.TextView() text_buffer = message.get_buffer() text_buffer.set_text(text) message.set_editable(False) messagde.set_justification(Gtk.Justification.LEFT) message.override_color(Gtk.StateType.Normal, text_color) message.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) msg_vbox.pack_start(message, True, True, 0) # Order of boxes for RTL languages: if lang_rtl: msg_vbox.reverse() if new_msg: rb.reverse() if new_msg: box = Gtk.Vbox() box.pack_start(rb, True, True, 2) 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 _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 sugar3 import profile from sugar3.activity.activity import show_object_in_journal from sugar3.datastore import datastore logger.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): GObject.GObject.__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): logger.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)): 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 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