From 5a31ba2ef0a71459466a327c09669ada91374431 Mon Sep 17 00:00:00 2001 From: Julien Pivotto <roidelapluie@esquimaux.be> Date: Fri, 14 Nov 2008 11:13:15 +0000 Subject: [PATCH] Very basic integration of xHtml GUI --- data/glade/message_window.glade | 80 +++++++++--- src/chat_control.py | 84 +++++++++++- src/gajim.py | 3 + src/groupchat_control.py | 8 +- src/message_control.py | 4 +- src/message_textview.py | 221 +++++++++++++++++++++++++++++++- 6 files changed, 367 insertions(+), 33 deletions(-) diff --git a/data/glade/message_window.glade b/data/glade/message_window.glade index 6bc5daa8cc..b3e2c3cc32 100644 --- a/data/glade/message_window.glade +++ b/data/glade/message_window.glade @@ -231,15 +231,36 @@ </packing> </child> <child> - <widget class="GtkVSeparator" id="vseparator1"> + <widget class="GtkButton" id="formattings_button"> <property name="visible">True</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="tooltip" translatable="yes">Show a list of formattings</property> + <property name="relief">GTK_RELIEF_NONE</property> + <property name="focus_on_click">False</property> + <property name="response_id">0</property> + <child> + <widget class="GtkImage" id="image10"> + <property name="visible">True</property> + <property name="stock">gtk-bold</property> + <property name="icon_size">1</property> + </widget> + </child> </widget> <packing> <property name="expand">False</property> <property name="position">1</property> </packing> </child> + <child> + <widget class="GtkVSeparator" id="vseparator1"> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">2</property> + </packing> + </child> <child> <widget class="GtkButton" id="add_to_roster_button"> <property name="can_focus">True</property> @@ -261,7 +282,7 @@ <packing> <property name="expand">False</property> <property name="fill">False</property> - <property name="position">2</property> + <property name="position">3</property> </packing> </child> <child> @@ -282,7 +303,7 @@ </widget> <packing> <property name="expand">False</property> - <property name="position">3</property> + <property name="position">4</property> </packing> </child> <child> @@ -304,7 +325,7 @@ </widget> <packing> <property name="expand">False</property> - <property name="position">4</property> + <property name="position">5</property> </packing> </child> <child> @@ -326,7 +347,7 @@ </widget> <packing> <property name="expand">False</property> - <property name="position">5</property> + <property name="position">6</property> </packing> </child> <child> @@ -348,7 +369,7 @@ </widget> <packing> <property name="expand">False</property> - <property name="position">6</property> + <property name="position">7</property> </packing> </child> <child> @@ -358,7 +379,7 @@ </widget> <packing> <property name="expand">False</property> - <property name="position">7</property> + <property name="position">8</property> </packing> </child> <child> @@ -380,7 +401,7 @@ </widget> <packing> <property name="expand">False</property> - <property name="position">8</property> + <property name="position">9</property> </packing> </child> <child> @@ -392,7 +413,7 @@ </child> </widget> <packing> - <property name="position">9</property> + <property name="position">10</property> </packing> </child> <child> @@ -438,7 +459,7 @@ </widget> <packing> <property name="expand">False</property> - <property name="position">10</property> + <property name="position">11</property> </packing> </child> </widget> @@ -679,15 +700,36 @@ </packing> </child> <child> - <widget class="GtkVSeparator" id="vseparator2"> + <widget class="GtkButton" id="formattings_button"> <property name="visible">True</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="tooltip" translatable="yes">Show a list of formattings</property> + <property name="relief">GTK_RELIEF_NONE</property> + <property name="focus_on_click">False</property> + <property name="response_id">0</property> + <child> + <widget class="GtkImage" id="image11"> + <property name="visible">True</property> + <property name="stock">gtk-bold</property> + <property name="icon_size">1</property> + </widget> + </child> </widget> <packing> <property name="expand">False</property> <property name="position">1</property> </packing> </child> + <child> + <widget class="GtkVSeparator" id="vseparator2"> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">2</property> + </packing> + </child> <child> <widget class="GtkButton" id="change_nick_button"> <property name="visible">True</property> @@ -707,7 +749,7 @@ <packing> <property name="expand">False</property> <property name="fill">False</property> - <property name="position">2</property> + <property name="position">3</property> </packing> </child> <child> @@ -729,7 +771,7 @@ <packing> <property name="expand">False</property> <property name="fill">False</property> - <property name="position">3</property> + <property name="position">4</property> </packing> </child> <child> @@ -752,7 +794,7 @@ <packing> <property name="expand">False</property> <property name="fill">False</property> - <property name="position">4</property> + <property name="position">5</property> </packing> </child> <child> @@ -774,7 +816,7 @@ <packing> <property name="expand">False</property> <property name="fill">False</property> - <property name="position">5</property> + <property name="position">6</property> </packing> </child> <child> @@ -784,7 +826,7 @@ </widget> <packing> <property name="expand">False</property> - <property name="position">6</property> + <property name="position">7</property> </packing> </child> <child> @@ -813,7 +855,7 @@ <packing> <property name="expand">False</property> <property name="fill">False</property> - <property name="position">7</property> + <property name="position">8</property> </packing> </child> <child> @@ -825,7 +867,7 @@ </child> </widget> <packing> - <property name="position">8</property> + <property name="position">9</property> </packing> </child> <child> @@ -872,7 +914,7 @@ <packing> <property name="expand">False</property> <property name="fill">False</property> - <property name="position">9</property> + <property name="position">10</property> </packing> </child> </widget> diff --git a/src/chat_control.py b/src/chat_control.py index d8a1d6e32f..8cf50b0bf4 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -258,6 +258,10 @@ def __init__(self, type_id, parent_win, widget_name, contact, acct, id = widget.connect('clicked', self._on_send_button_clicked) self.handlers[id] = widget + widget = self.xml.get_widget('formattings_button') + id = widget.connect('clicked', self.on_formattings_button_clicked) + self.handlers[id] = widget + # the following vars are used to keep history of user's messages self.sent_history = [] self.sent_history_pos = 0 @@ -359,9 +363,10 @@ def _on_send_button_clicked(self, widget): start_iter = message_buffer.get_start_iter() end_iter = message_buffer.get_end_iter() message = message_buffer.get_text(start_iter, end_iter, 0).decode('utf-8') + xhtml = self.msg_textview.get_xhtml() # send the message - self.send_message(message) + self.send_message(message, xhtml=xhtml) def _paint_banner(self): '''Repaint banner with theme color''' @@ -508,6 +513,7 @@ def _on_message_textview_mykeypress_event(self, widget, event_keyval, start_iter, end_iter = message_buffer.get_bounds() message = message_buffer.get_text(start_iter, end_iter, False).decode( 'utf-8') + xhtml = self.msg_textview.get_xhtml() # construct event instance from binding event = gtk.gdk.Event(gtk.gdk.KEY_PRESS) # it's always a key-press here @@ -551,7 +557,7 @@ def _on_message_textview_mykeypress_event(self, widget, event_keyval, send_message = False if send_message: - self.send_message(message) # send the message + self.send_message(message, xhtml=xhtml) # send the message else: # Give the control itself a chance to process self.handle_message_textview_mykey_press(widget, event_keyval, @@ -595,7 +601,7 @@ def _process_command(self, message): def send_message(self, message, keyID = '', type_ = 'chat', chatstate = None, msg_id = None, composing_xep = None, resource = None, - process_command = True): + process_command = True, xhtml = None): '''Send the given message to the active tab. Doesn't return None if error ''' if not message or message == '\n': @@ -607,7 +613,7 @@ def send_message(self, message, keyID = '', type_ = 'chat', chatstate = None, ret = MessageControl.send_message(self, message, keyID, type_ = type_, chatstate = chatstate, msg_id = msg_id, composing_xep = composing_xep, resource = resource, - user_nick = self.user_nick) + user_nick = self.user_nick, xhtml = xhtml) # Record message history self.save_sent_message(message) @@ -739,6 +745,68 @@ def on_emoticons_button_clicked(self, widget): gajim.interface.emoticon_menuitem_clicked = self.append_emoticon gajim.interface.popup_emoticons_under_button(widget, self.parent_win) + def on_formattings_button_clicked(self, widget): + '''popup formattings menu''' + menu = gtk.Menu() + + menuitems = ((_('Bold'), 'bold'), + (_('Italic'), 'italic'), + (_('Underline'), 'underline'), + (_('Strike'), 'strike')) + + active_tags = self.msg_textview.get_active_tags() + + for menuitem in menuitems: + item = gtk.CheckMenuItem(menuitem[0]) + if menuitem[1] in active_tags: + item.set_active(True) + else: + item.set_active(False) + item.connect('activate', self.msg_textview.set_tag, + menuitem[1]) + menu.append(item) + + item = gtk.SeparatorMenuItem() # separator + menu.append(item) + + item = gtk.ImageMenuItem(_('Color')) + icon = gtk.image_new_from_stock(gtk.STOCK_SELECT_COLOR, gtk.ICON_SIZE_MENU) + item.set_image(icon) + item.connect('activate', self.on_color_menuitem_activale) + menu.append(item) + + item = gtk.ImageMenuItem(_('Font')) + icon = gtk.image_new_from_stock(gtk.STOCK_SELECT_FONT, gtk.ICON_SIZE_MENU) + item.set_image(icon) + item.connect('activate', self.on_font_menuitem_activale) + menu.append(item) + + item = gtk.SeparatorMenuItem() # separator + menu.append(item) + + item = gtk.ImageMenuItem(_('Clear formating')) + icon = gtk.image_new_from_stock(gtk.STOCK_CLEAR, gtk.ICON_SIZE_MENU) + item.set_image(icon) + item.connect('activate', self.msg_textview.clear_tags) + menu.append(item) + + menu.show_all() + gtkgui_helpers.popup_emoticons_under_button(menu, widget, + self.parent_win) + + def on_color_menuitem_activale(self, widget): + color_dialog = gtk.ColorSelectionDialog('Select a color') + color_dialog.connect('response', self.msg_textview.color_set, + color_dialog.colorsel) + color_dialog.show_all() + + def on_font_menuitem_activale(self, widget): + font_dialog = gtk.FontSelectionDialog('Select a font') + font_dialog.connect('response', self.msg_textview.font_set, + font_dialog.fontsel) + font_dialog.show_all() + + def on_actions_button_clicked(self, widget): '''popup action menu''' menu = self.prepare_context_menu(True) @@ -1668,7 +1736,8 @@ def get_command_help(self, command): else: self.print_conversation(_('No help info for /%s') % command, 'info') - def send_message(self, message, keyID = '', chatstate = None): + def send_message(self, message, keyID = '', chatstate = None, + xhtml = None): '''Send a message to contact''' if message in ('', None, '\n') or self._process_command(message): return None @@ -1725,7 +1794,7 @@ def send_message(self, message, keyID = '', chatstate = None): id = ChatControlBase.send_message(self, message, keyID, type_ = 'chat', chatstate = chatstate_to_send, composing_xep = composing_xep, - process_command = process_command) + process_command = process_command, xhtml = xhtml) if id: # XXX: Once we have fallback to disco, remove # notexistant check @@ -1738,7 +1807,8 @@ def send_message(self, message, keyID = '', chatstate = None): xep0184_id = None self.print_conversation(message, self.contact.jid, - encrypted = encrypted, xep0184_id = xep0184_id) + encrypted = encrypted, xep0184_id = xep0184_id, + xhtml = xhtml) def check_for_possible_paused_chatstate(self, arg): ''' did we move mouse of that window or write something in message diff --git a/src/gajim.py b/src/gajim.py index acc6038377..a48898580f 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -2453,6 +2453,9 @@ def make_regexps(self): latex = r'|\$\$[^$\\]*?([\]\[0-9A-Za-z()|+*/-]|[\\][\]\[0-9A-Za-z()|{}$])(.*?[^\\])?\$\$' basic_pattern = links + '|' + mail + '|' + legacy_prefixes + + link_pattern = basic_pattern + self.link_pattern_re = re.compile(link_pattern, re.IGNORECASE) if gajim.config.get('use_latex'): basic_pattern += latex diff --git a/src/groupchat_control.py b/src/groupchat_control.py index 31421d657d..91facdd30a 100644 --- a/src/groupchat_control.py +++ b/src/groupchat_control.py @@ -127,7 +127,7 @@ def __init__(self, parent_win, gc_contact, contact, account, session): ChatControl.__init__(self, parent_win, contact, account, session) self.TYPE_ID = 'pm' - def send_message(self, message): + def send_message(self, message, xhtml=None): '''call this function to send our message''' if not message: return @@ -153,7 +153,7 @@ def send_message(self, message): 'left.') % {'room': room, 'nick': nick}) return - ChatControl.send_message(self, message) + ChatControl.send_message(self, message, xhtml=xhtml) def update_ui(self): if self.contact.show == 'offline': @@ -1628,7 +1628,7 @@ def _process_command(self, message): return False - def send_message(self, message): + def send_message(self, message, xhtml=None): '''call this function to send our message''' if not message: return @@ -1644,7 +1644,7 @@ def send_message(self, message): if not self._process_command(message): # Send the message gajim.connections[self.account].send_gc_message(self.room_jid, - message) + message, xhtml=xhtml) self.msg_textview.get_buffer().set_text('') self.msg_textview.grab_focus() diff --git a/src/message_control.py b/src/message_control.py index 7172b7e65d..26b6e84e12 100644 --- a/src/message_control.py +++ b/src/message_control.py @@ -162,7 +162,7 @@ def set_session(self, session): def send_message(self, message, keyID = '', type_ = 'chat', chatstate = None, msg_id = None, composing_xep = None, resource = None, - user_nick = None): + user_nick = None, xhtml = None): # Send the given message to the active tab. # Doesn't return None if error jid = self.contact.jid @@ -189,6 +189,6 @@ def send_message(self, message, keyID = '', type_ = 'chat', composing_xep = composing_xep, resource = self.resource, user_nick = user_nick, session = self.session, - original_message = original_message) + original_message = original_message, xhtml = xhtml) # vim: se ts=3: diff --git a/src/message_textview.py b/src/message_textview.py index 334bdac933..1f37e3f624 100644 --- a/src/message_textview.py +++ b/src/message_textview.py @@ -22,6 +22,9 @@ import gtk import gobject +import pango +import gtkgui_helpers +from common import gajim class MessageTextView(gtk.TextView): '''Class for the message textview (where user writes new messages) @@ -35,7 +38,7 @@ class MessageTextView(gtk.TextView): def __init__(self): gtk.TextView.__init__(self) - + # set properties self.set_border_width(1) self.set_accepts_tab(True) @@ -48,6 +51,222 @@ def __init__(self): self.set_pixels_below_lines(2) self.lang = None # Lang used for spell checking + buffer = self.get_buffer() + self.begin_tags = {} + self.end_tags = {} + self.color_tags = [] + self.fonts_tags = [] + self.other_tags = {} + self.other_tags['bold'] = buffer.create_tag('bold') + self.other_tags['bold'].set_property('weight', pango.WEIGHT_BOLD) + self.begin_tags['bold'] = '<strong>' + self.end_tags['bold'] = '</strong>' + self.other_tags['italic'] = buffer.create_tag('italic') + self.other_tags['italic'].set_property('style', pango.STYLE_ITALIC) + self.begin_tags['italic'] = '<em>' + self.end_tags['italic'] = '</em>' + self.other_tags['underline'] = buffer.create_tag('underline') + self.other_tags['underline'].set_property('underline', pango.UNDERLINE_SINGLE) + self.begin_tags['underline'] = '<span style="text-decoration: underline;">' + self.end_tags['underline'] = '</span>' + self.other_tags['strike'] = buffer.create_tag('strike') + self.other_tags['strike'].set_property('strikethrough', True) + self.begin_tags['strike'] = '<span style="text-decoration: line-through;">' + self.end_tags['strike'] = '</span>' + + def make_clickable_urls(self, text): + buffer = self.get_buffer() + + start = 0 + end = 0 + index = 0 + + new_text = '' + iterator = gajim.interface.link_pattern_re.finditer(text) + for match in iterator: + start, end = match.span() + url = text[start:end] + if start != 0: + text_before_special_text = text[index:start] + else: + text_before_special_text = '' + end_iter = buffer.get_end_iter() + # we insert normal text + new_text += text_before_special_text + \ + '<a href="'+ url +'">' + url + '</a>' + + index = end # update index + + if end < len(text): + new_text += text[end:] + + return new_text # the position after *last* special text + + def get_active_tags(self): + buffer = self.get_buffer() + return_val = buffer.get_selection_bounds() + if return_val: # if sth was selected + start, finish = return_val[0], return_val[1] + else: + start, finish = buffer.get_bounds() + active_tags = [] + for tag in start.get_tags(): + active_tags.append(tag.get_property('name')) + return active_tags + + def set_tag(self, widget, tag): + buffer = self.get_buffer() + return_val = buffer.get_selection_bounds() + if return_val: # if sth was selected + start, finish = return_val[0], return_val[1] + else: + start, finish = buffer.get_bounds() + if start.has_tag(self.other_tags[tag]): + buffer.remove_tag_by_name(tag, start, finish) + else: + if tag == 'underline': + buffer.remove_tag_by_name('strike', start, finish) + elif tag == 'strike': + buffer.remove_tag_by_name('underline', start, finish) + buffer.apply_tag_by_name(tag, start, finish) + + def clear_tags(self, widget): + buffer = self.get_buffer() + return_val = buffer.get_selection_bounds() + if return_val: # if sth was selected + start, finish = return_val[0], return_val[1] + else: + start, finish = buffer.get_bounds() + buffer.remove_all_tags(start, finish) + + def color_set(self, widget, response, color): + if response == -6: + widget.destroy() + return + buffer = self.get_buffer() + color = color.get_current_color() + widget.destroy() + color_string = gtkgui_helpers.make_color_string(color) + tag_name = 'color' + color_string + if not tag_name in self.color_tags: + tagColor = buffer.create_tag(tag_name) + tagColor.set_property('foreground', color_string) + self.begin_tags[tag_name] = '<span style="color: ' + color_string + ';">' + self.end_tags[tag_name] = '</span>' + self.color_tags.append(tag_name) + + return_val = buffer.get_selection_bounds() + if return_val: # if sth was selected + start, finish = return_val[0], return_val[1] + else: + start, finish = buffer.get_bounds() + + for tag in self.color_tags: + buffer.remove_tag_by_name(tag, start, finish) + + buffer.apply_tag_by_name(tag_name, start, finish) + + def font_set(self, widget, response, font): + if response == -6: + widget.destroy() + return + + buffer = self.get_buffer() + + font = font.get_font_name() + font_desc = pango.FontDescription(font) + family = font_desc.get_family() + size = font_desc.get_size() + size = size / pango.SCALE + weight = font_desc.get_weight() + style = font_desc.get_style() + + widget.destroy() + + tag_name = 'font' + font + if not tag_name in self.fonts_tags: + tagFont = buffer.create_tag(tag_name) + tagFont.set_property('font', family + ' ' + str(size)) + self.begin_tags[tag_name] = \ + '<span style="font-family: ' + family + '; ' + \ + 'font-size: ' + str(size) + 'px">' + self.end_tags[tag_name] = '</span>' + self.fonts_tags.append(tag_name) + + return_val = buffer.get_selection_bounds() + if return_val: # if sth was selected + start, finish = return_val[0], return_val[1] + else: + start, finish = buffer.get_bounds() + + for tag in self.fonts_tags: + buffer.remove_tag_by_name(tag, start, finish) + + buffer.apply_tag_by_name(tag_name, start, finish) + + if weight == pango.WEIGHT_BOLD: + buffer.apply_tag_by_name('bold', start, finish) + else: + buffer.remove_tag_by_name('bold', start, finish) + + if style == pango.STYLE_ITALIC: + buffer.apply_tag_by_name('italic', start, finish) + else: + buffer.remove_tag_by_name('italic', start, finish) + + def get_xhtml(self): + buffer = self.get_buffer() + old = buffer.get_start_iter() + tags = {} + tags['bold'] = False + iter = buffer.get_start_iter() + old = buffer.get_start_iter() + texte = '' + modified = False + def xhtml_special(text): + text = text.replace('<', '<') + text = text.replace('>', '>') + text = text.replace('\n', '<br />') + return text + + for tag in iter.get_toggled_tags(True): + texte += self.begin_tags[tag.get_property('name')] + modified = True + while (iter.forward_to_tag_toggle(None) and not iter.is_end()): + modified = True + texte += xhtml_special(buffer.get_text(old, iter)) + old.forward_to_tag_toggle(None) + new_tags = [] + old_tags = [] + end_tags = [] + for tag in iter.get_toggled_tags(True): + new_tags.append(tag.get_property('name')) + + for tag in iter.get_tags(): + if tag.get_property('name') not in new_tags: + old_tags.append(tag.get_property('name')) + + for tag in iter.get_toggled_tags(False): + end_tags.append(tag.get_property('name')) + + for tag in old_tags: + texte += self.end_tags[tag] + for tag in end_tags: + texte += self.end_tags[tag] + for tag in new_tags: + texte += self.begin_tags[tag] + for tag in old_tags: + texte += self.begin_tags[tag] + + texte += xhtml_special(buffer.get_text(old, buffer.get_end_iter())) + for tag in iter.get_toggled_tags(False): + texte += self.end_tags[tag.get_property('name')] + + if modified: + return '<p>' + self.make_clickable_urls(texte) + '</p>' + else: + return None + def destroy(self): import gc -- GitLab