diff options
-rw-r--r-- | chat/box.py | 315 | ||||
-rw-r--r-- | chat/smilies.py | 39 |
2 files changed, 199 insertions, 155 deletions
diff --git a/chat/box.py b/chat/box.py index c3db97d..77f2bf5 100644 --- a/chat/box.py +++ b/chat/box.py @@ -26,10 +26,10 @@ from os.path import join import gtk import pango -import cairo +from gobject import SIGNAL_RUN_FIRST, TYPE_PYOBJECT from sugar.graphics import style -from sugar.graphics.palette import Palette +from sugar.graphics.palette import Palette, MouseSpeedDetector from sugar.presence import presenceservice from sugar.graphics.menuitem import MenuItem from sugar.activity.activity import get_activity_root, show_object_in_journal @@ -46,6 +46,174 @@ _URL_REGEXP = re.compile('((http|ftp)s?://)?' '(:[1-9][0-9]{0,4})?(/[-a-zA-Z0-9/%~@&_+=;:,.?#]*[a-zA-Z0-9/])?') +class TextBox(gtk.TextView): + + __gsignals__ = { + 'mouse-enter': (SIGNAL_RUN_FIRST, None, [TYPE_PYOBJECT]), + 'mouse-leave': (SIGNAL_RUN_FIRST, None, [TYPE_PYOBJECT]), + 'right-click': (SIGNAL_RUN_FIRST, None, [TYPE_PYOBJECT]), + } + + hand_cursor = gtk.gdk.Cursor(gtk.gdk.HAND2) + + def __init__(self, color, bg_color, lang_rtl): + self._lang_rtl = lang_rtl + gtk.TextView.__init__(self) + self.set_editable(False) + self.set_cursor_visible(False) + self.set_wrap_mode(gtk.WRAP_WORD_CHAR) + self.get_buffer().set_text("", 0) + self.iter_text = self.get_buffer().get_iter_at_offset(0) + self.fg_tag = self.get_buffer().create_tag("foreground_color", + foreground=color.get_html()) + self._empty = True + self.palette = None + self._mouse_detector = MouseSpeedDetector(self, 200, 5) + self._mouse_detector.connect('motion-slow', self._mouse_slow_cb) + self.modify_base(gtk.STATE_NORMAL, bg_color.get_gdk_color()) + self.connect("event-after", self.event_after) + self.motion_notify_id = self.connect("motion-notify-event", \ + self.motion_notify_event) + self.connect("visibility-notify-event", self.visibility_notify_event) + + # Links can be activated by clicking. + def event_after(self, widget, event): + if event.type != gtk.gdk.BUTTON_RELEASE: + return False + if event.button == 2: + palette = tag.get_data('palette') + #xw, yw = self.get_toplevel().get_pointer() + logging.error('Popop palette by secondary button click') + palette.move(event.x, event.y) + palette.popup() + return False + + x, y = self.window_to_buffer_coords(gtk.TEXT_WINDOW_WIDGET, + int(event.x), int(event.y)) + iter_tags = self.get_iter_at_location(x, y) + + for tag in iter_tags.get_tags(): + url = tag.get_data('url') + if url is not None: + self._show_via_journal(url) + + return False + + 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) + + # Looks at all tags covering the position (x, y) in the text view, + # and if one of them is a link return True + def check_url_hovering(self, x, y): + hovering = False + self.palette = None + iter_tags = self.get_iter_at_location(x, y) + + tags = iter_tags.get_tags() + for tag in tags: + url = tag.get_data('url') + self.palette = tag.get_data('palette') + if url is not None: + hovering = True + break + return hovering + + # Looks at all tags covering the position (x, y) in the text view, + # and if one of them is a link, change the cursor to the "hands" cursor + def set_cursor_if_appropriate(self, x, y): + hovering_over_link = self.check_url_hovering(x, y) + win = self.get_window(gtk.TEXT_WINDOW_TEXT) + if hovering_over_link: + win.set_cursor(self.hand_cursor) + self._mouse_detector.start() + else: + win.set_cursor(None) + self._mouse_detector.stop() + + def _mouse_slow_cb(self, widget): + x, y = self.get_pointer() + hovering_over_link = self.check_url_hovering(x, y) + if hovering_over_link: + if self.palette is not None: + xw, yw = self.get_toplevel().get_pointer() + logging.error('move palette to %d %d' % (xw, yw)) + self.palette.move(xw, yw) + self.palette.popup() + else: + if self.palette is not None: + self.palette.popdown() + + # Update the cursor image if the pointer moved. + def motion_notify_event(self, widget, event): + x, y = self.window_to_buffer_coords(gtk.TEXT_WINDOW_WIDGET, + int(event.x), int(event.y)) + self.set_cursor_if_appropriate(x, y) + self.window.get_pointer() + return False + + # Also update the cursor image if the window becomes visible + # (e.g. when a window covering it got iconified). + def visibility_notify_event(self, widget, event): + wx, wy, mod = self.window.get_pointer() + bx, by = self.window_to_buffer_coords(gtk.TEXT_WINDOW_WIDGET, wx, wy) + self.set_cursor_if_appropriate(bx, by) + return False + + def __palette_mouse_enter_cb(self, widget, event): + self.handler_block(self.motion_notify_id) + + def __palette_mouse_leave_cb(self, widget, event): + self.handler_unblock(self.motion_notify_id) + + def add_text(self, text): + if not self._empty: + self.get_buffer().insert(self.iter_text, '\n') + + words = text.split() + for word in words: + if _URL_REGEXP.search(word) is not None: + tag = self.get_buffer().create_tag(None, + foreground="blue", underline=pango.UNDERLINE_SINGLE) + tag.set_data("url", word) + palette = _URLMenu(word) + palette.connect('enter-notify-event', + self.__palette_mouse_enter_cb) + palette.connect('leave-notify-event', + self.__palette_mouse_leave_cb) + tag.set_data('palette', palette) + self.get_buffer().insert_with_tags(self.iter_text, word, tag, + self.fg_tag) + else: + smile_pxb = smilies.get_pixbuf(word) + if smile_pxb is not None: + self.get_buffer().insert_pixbuf(self.iter_text, smile_pxb) + else: + self.get_buffer().insert_with_tags(self.iter_text, word, + self.fg_tag) + self.get_buffer().insert_with_tags(self.iter_text, ' ', + self.fg_tag) + + self._empty = False + + class ColorLabel(gtk.Label): def __init__(self, text, color=None): @@ -58,17 +226,6 @@ class ColorLabel(gtk.Label): self.set_markup(text) -class LinkLabel(ColorLabel): - - def __init__(self, text, color=None): - self.text = '<a href="%s">' % text + \ - text + '</a>' - ColorLabel.__init__(self, self.text, color) - - def create_palette(self): - return _URLMenu(self.text) - - class ChatBox(gtk.ScrolledWindow): def __init__(self): @@ -86,13 +243,12 @@ class ChatBox(gtk.ScrolledWindow): self._conversation = gtk.VBox() self._conversation.set_homogeneous(False) - #self._conversation.background_color=style.COLOR_WHITE - #spacing=0, - #box_width=-1, # natural width - #background_color=style.COLOR_WHITE.get_int()) + evbox = gtk.EventBox() + evbox.modify_bg(gtk.STATE_NORMAL, style.COLOR_WHITE.get_gdk_color()) + evbox.add(self._conversation) - self.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) - self.add_with_viewport(self._conversation) + self.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS) + self.add_with_viewport(evbox) vadj = self.get_vadjustment() vadj.connect('changed', self._scroll_changed_cb) vadj.connect('value-changed', self._scroll_value_changed_cb) @@ -112,15 +268,11 @@ class ChatBox(gtk.ScrolledWindow): True: show what buddy did .------------- rb ---------------. - | +name_vbox+ +----msg_vbox----+ | + | +name_vbox+ +----align-----+ | | | | | | | - | | nick: | | +--msg_hbox--+ | | - | | | | | text | | | + | | nick: | | +--message---+ | | + | | | | | text | | | | +---------+ | +------------+ | | - | | | | - | | +--msg_hbox--+ | | - | | | text | url | | | - | | +------------+ | | | +----------------+ | `--------------------------------' """ @@ -168,91 +320,32 @@ class ChatBox(gtk.ScrolledWindow): new_msg = False if not new_msg: - rb = self._last_msg - msg_vbox = rb.get_children()[1] - msg_hbox = gtk.HBox() - msg_hbox.show() - msg_vbox.pack_start(msg_hbox, True, True) + message = self._last_msg else: rb = RoundBox() + screen_width = gtk.gdk.screen_width() + # keep space to the scrollbar + rb.set_size_request(screen_width - 50, -1) rb.background_color = color_fill rb.border_color = color_stroke - self._last_msg = rb self._last_msg_sender = buddy - if not status_message: name = ColorLabel(text=nick + ': ', color=text_color) name_vbox = gtk.VBox() name_vbox.pack_start(name, False, False) - rb.pack_start(name_vbox, False, False) - msg_vbox = gtk.VBox() - rb.pack_start(msg_vbox, False, False) - msg_hbox = gtk.HBox() - msg_vbox.pack_start(msg_hbox, False, False) + rb.pack_start(name_vbox, False, False, padding=5) + + message = TextBox(text_color, color_fill, lang_rtl) + vbox = gtk.VBox() + vbox.pack_start(message, True, True, padding=5) + rb.pack_start(vbox, True, True, padding=5) + self._last_msg = message + self._conversation.pack_start(rb, False, False, padding=2) 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 = ColorLabel( - text=starttext, - color=text_color) - msg_hbox.pack_start(message, True, True) - message.show() - url = text[match.start():match.end()] - - message = LinkLabel( - text=url, - color=text_color) - message.connect('activate-link', self._link_activated_cb) - - align = gtk.Alignment(xalign=0.0, yalign=0.0, xscale=0.0, - yscale=0.0) - align.add(message) - - msg_hbox.pack_start(align, True, True) - msg_hbox.show() - text = text[match.end():] - match = _URL_REGEXP.search(text) - - if text: - for word in smilies.parse(text): - if isinstance(word, cairo.ImageSurface): - pass - # TODO: - """ - 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 = ColorLabel( - text=word, - color=text_color) - item.show() - align = gtk.Alignment(xalign=0.0, yalign=0.0, xscale=0.0, - yscale=0.0) - align.add(item) - msg_hbox.pack_start(align, True, True) - - # Order of boxes for RTL languages: - if lang_rtl: - msg_hbox.reverse() - if new_msg: - rb.reverse() - - if new_msg: - box = RoundBox() # TODO: padding=2) - box.show() - box.pack_start(rb, True, True) - self._conversation.pack_start(box, False, False) + message.add_text(text) self._conversation.show_all() def add_separator(self, timestamp): @@ -326,32 +419,6 @@ class ChatBox(gtk.ScrolledWindow): adj.set_value(adj.upper - adj.page_size) self._scroll_value = adj.get_value() - def _link_activated_cb(self, label, link): - url = _url_check_protocol(link.props.text) - self._show_via_journal(url) - return False - - 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 _URLMenu(Palette): diff --git a/chat/smilies.py b/chat/smilies.py index 7248ed6..dfb608a 100644 --- a/chat/smilies.py +++ b/chat/smilies.py @@ -25,7 +25,6 @@ import cairo from sugar.graphics import style from sugar.activity.activity import get_activity_root, get_bundle_path - THEME = [ # TRANS: A smiley (http://en.wikipedia.org/wiki/Smiley) explanation # TRANS: ASCII-art equivalents are :-) and :) @@ -97,6 +96,14 @@ SMILIES_SIZE = int(style.STANDARD_ICON_SIZE * 0.75) _catalog = None +def get_pixbuf(word): + """Return a pixbuf associated to a smile, or None if not available""" + for (name, hint, codes) in THEME: + if word in codes: + return gtk.gdk.pixbuf_new_from_file(name) + return None + + def init(): """Initialise smilies data.""" global _catalog @@ -125,36 +132,6 @@ def init(): 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.""" |