diff --git a/data/glade/message_window.glade b/data/glade/message_window.glade
index 6bc5daa8c..b3e2c3cc3 100644
--- a/data/glade/message_window.glade
+++ b/data/glade/message_window.glade
@@ -230,6 +230,27 @@
False
+
+
+ True
+ GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
+ Show a list of formattings
+ GTK_RELIEF_NONE
+ False
+ 0
+
+
+ True
+ gtk-bold
+ 1
+
+
+
+
+ False
+ 1
+
+
True
@@ -237,7 +258,7 @@
False
- 1
+ 2
@@ -261,7 +282,7 @@
False
False
- 2
+ 3
@@ -282,7 +303,7 @@
False
- 3
+ 4
@@ -304,7 +325,7 @@
False
- 4
+ 5
@@ -326,7 +347,7 @@
False
- 5
+ 6
@@ -348,7 +369,7 @@
False
- 6
+ 7
@@ -358,7 +379,7 @@
False
- 7
+ 8
@@ -380,7 +401,7 @@
False
- 8
+ 9
@@ -392,7 +413,7 @@
- 9
+ 10
@@ -438,7 +459,7 @@
False
- 10
+ 11
@@ -678,6 +699,27 @@
False
+
+
+ True
+ GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
+ Show a list of formattings
+ GTK_RELIEF_NONE
+ False
+ 0
+
+
+ True
+ gtk-bold
+ 1
+
+
+
+
+ False
+ 1
+
+
True
@@ -685,7 +727,7 @@
False
- 1
+ 2
@@ -707,7 +749,7 @@
False
False
- 2
+ 3
@@ -729,7 +771,7 @@
False
False
- 3
+ 4
@@ -752,7 +794,7 @@
False
False
- 4
+ 5
@@ -774,7 +816,7 @@
False
False
- 5
+ 6
@@ -784,7 +826,7 @@
False
- 6
+ 7
@@ -813,7 +855,7 @@
False
False
- 7
+ 8
@@ -825,7 +867,7 @@
- 8
+ 9
@@ -872,7 +914,7 @@
False
False
- 9
+ 10
diff --git a/src/chat_control.py b/src/chat_control.py
index d8a1d6e32..8cf50b0bf 100644
--- a/src/chat_control.py
+++ b/src/chat_control.py
@@ -258,6 +258,10 @@ class ChatControlBase(MessageControl):
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 @@ class ChatControlBase(MessageControl):
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 @@ class ChatControlBase(MessageControl):
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 @@ class ChatControlBase(MessageControl):
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 @@ class ChatControlBase(MessageControl):
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 @@ class ChatControlBase(MessageControl):
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 @@ class ChatControlBase(MessageControl):
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 @@ class ChatControl(ChatControlBase):
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 @@ class ChatControl(ChatControlBase):
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 @@ class ChatControl(ChatControlBase):
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 acc603837..a48898580 100755
--- a/src/gajim.py
+++ b/src/gajim.py
@@ -2453,6 +2453,9 @@ class Interface:
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 31421d657..91facdd30 100644
--- a/src/groupchat_control.py
+++ b/src/groupchat_control.py
@@ -127,7 +127,7 @@ class PrivateChatControl(ChatControl):
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 @@ class PrivateChatControl(ChatControl):
'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 @@ class GroupchatControl(ChatControlBase):
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 @@ class GroupchatControl(ChatControlBase):
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 7172b7e65..26b6e84e1 100644
--- a/src/message_control.py
+++ b/src/message_control.py
@@ -162,7 +162,7 @@ class MessageControl:
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 @@ class MessageControl:
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 334bdac93..1f37e3f62 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 @@ class MessageTextView(gtk.TextView):
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'] = ''
+ self.end_tags['bold'] = ''
+ self.other_tags['italic'] = buffer.create_tag('italic')
+ self.other_tags['italic'].set_property('style', pango.STYLE_ITALIC)
+ self.begin_tags['italic'] = ''
+ self.end_tags['italic'] = ''
+ self.other_tags['underline'] = buffer.create_tag('underline')
+ self.other_tags['underline'].set_property('underline', pango.UNDERLINE_SINGLE)
+ self.begin_tags['underline'] = ''
+ self.end_tags['underline'] = ''
+ self.other_tags['strike'] = buffer.create_tag('strike')
+ self.other_tags['strike'].set_property('strikethrough', True)
+ self.begin_tags['strike'] = ''
+ self.end_tags['strike'] = ''
+
+ 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 + \
+ '' + url + ''
+
+ 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] = ''
+ self.end_tags[tag_name] = ''
+ 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] = \
+ ''
+ self.end_tags[tag_name] = ''
+ 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', '
')
+ 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 '
' + self.make_clickable_urls(texte) + '
'
+ else:
+ return None
+
def destroy(self):
import gc