diff --git a/src/chat_control.py b/src/chat_control.py
index 890bc5c9c..5f0801afc 100644
--- a/src/chat_control.py
+++ b/src/chat_control.py
@@ -424,7 +424,8 @@ class ChatControlBase(MessageControl):
message_textview = widget
message_buffer = message_textview.get_buffer()
start_iter, end_iter = message_buffer.get_bounds()
- message = message_buffer.get_text(start_iter, end_iter, False).decode('utf-8')
+ message = message_buffer.get_text(start_iter, end_iter, False).decode(
+ 'utf-8')
# construct event instance from binding
event = gtk.gdk.Event(gtk.gdk.KEY_PRESS) # it's always a key-press here
@@ -470,7 +471,8 @@ class ChatControlBase(MessageControl):
self.send_message(message) # send the message
else:
# Give the control itself a chance to process
- self.handle_message_textview_mykey_press(widget, event_keyval, event_keymod)
+ self.handle_message_textview_mykey_press(widget, event_keyval,
+ event_keymod)
def _process_command(self, message):
if not message:
@@ -533,7 +535,7 @@ class ChatControlBase(MessageControl):
def print_conversation_line(self, text, kind, name, tim,
other_tags_for_name = [], other_tags_for_time = [],
other_tags_for_text = [], count_as_new = True,
- subject = None, old_kind = None):
+ subject = None, old_kind = None, xhtml = None):
'''prints 'chat' type messages'''
jid = self.contact.jid
full_jid = self.get_full_jid()
@@ -543,7 +545,7 @@ class ChatControlBase(MessageControl):
end = True
textview.print_conversation_line(text, jid, kind, name, tim,
other_tags_for_name, other_tags_for_time, other_tags_for_text,
- subject, old_kind)
+ subject, old_kind, xhtml)
if not count_as_new:
return
@@ -765,7 +767,8 @@ class ChatControlBase(MessageControl):
#whatever is already typed
start_iter = conv_buf.get_start_iter()
end_iter = conv_buf.get_end_iter()
- self.orig_msg = conv_buf.get_text(start_iter, end_iter, 0).decode('utf-8')
+ self.orig_msg = conv_buf.get_text(start_iter, end_iter, 0).decode(
+ 'utf-8')
self.typing_new = False
if direction == 'up':
if self.sent_history_pos == 0:
@@ -825,8 +828,8 @@ class ChatControl(ChatControlBase):
old_msg_kind = None # last kind of the printed message
def __init__(self, parent_win, contact, acct, resource = None):
- ChatControlBase.__init__(self, self.TYPE_ID, parent_win, 'chat_child_vbox',
- (_('Chat'), _('Chats')), contact, acct, resource)
+ ChatControlBase.__init__(self, self.TYPE_ID, parent_win,
+ 'chat_child_vbox', (_('Chat'), _('Chats')), contact, acct, resource)
# for muc use:
# widget = self.xml.get_widget('muc_window_actions_button')
@@ -834,13 +837,16 @@ class ChatControl(ChatControlBase):
id = widget.connect('clicked', self.on_actions_button_clicked)
self.handlers[id] = widget
- self.hide_chat_buttons_always = gajim.config.get('always_hide_chat_buttons')
+ self.hide_chat_buttons_always = gajim.config.get(
+ 'always_hide_chat_buttons')
self.chat_buttons_set_visible(self.hide_chat_buttons_always)
- self.widget_set_visible(self.xml.get_widget('banner_eventbox'), gajim.config.get('hide_chat_banner'))
+ self.widget_set_visible(self.xml.get_widget('banner_eventbox'),
+ gajim.config.get('hide_chat_banner'))
# Initialize drag-n-drop
self.TARGET_TYPE_URI_LIST = 80
self.dnd_list = [ ( 'text/uri-list', 0, self.TARGET_TYPE_URI_LIST ) ]
- id = self.widget.connect('drag_data_received', self._on_drag_data_received)
+ id = self.widget.connect('drag_data_received',
+ self._on_drag_data_received)
self.handlers[id] = self.widget
self.widget.drag_dest_set(gtk.DEST_DEFAULT_MOTION |
gtk.DEST_DEFAULT_HIGHLIGHT |
@@ -862,17 +868,21 @@ class ChatControl(ChatControlBase):
self._on_window_motion_notify)
self.handlers[id] = self.parent_win.window
message_tv_buffer = self.msg_textview.get_buffer()
- id = message_tv_buffer.connect('changed', self._on_message_tv_buffer_changed)
+ id = message_tv_buffer.connect('changed',
+ self._on_message_tv_buffer_changed)
self.handlers[id] = message_tv_buffer
widget = self.xml.get_widget('avatar_eventbox')
- id = widget.connect('enter-notify-event', self.on_avatar_eventbox_enter_notify_event)
+ id = widget.connect('enter-notify-event',
+ self.on_avatar_eventbox_enter_notify_event)
self.handlers[id] = widget
- id = widget.connect('leave-notify-event', self.on_avatar_eventbox_leave_notify_event)
+ id = widget.connect('leave-notify-event',
+ self.on_avatar_eventbox_leave_notify_event)
self.handlers[id] = widget
- id = widget.connect('button-press-event', self.on_avatar_eventbox_button_press_event)
+ id = widget.connect('button-press-event',
+ self.on_avatar_eventbox_button_press_event)
self.handlers[id] = widget
widget = self.xml.get_widget('gpg_togglebutton')
@@ -1166,8 +1176,8 @@ class ChatControl(ChatControlBase):
if current_state == 'composing':
self.send_chatstate('paused') # pause composing
- # assume no activity and let the motion-notify or 'insert-text' make them True
- # refresh 30 seconds vars too or else it's 30 - 5 = 25 seconds!
+ # assume no activity and let the motion-notify or 'insert-text' make them
+ # True refresh 30 seconds vars too or else it's 30 - 5 = 25 seconds!
self.reset_kbd_mouse_timeout_vars()
return True # loop forever
@@ -1186,11 +1196,12 @@ class ChatControl(ChatControlBase):
if self.mouse_over_in_last_5_secs or self.kbd_activity_in_last_5_secs:
return True # loop forever
- if not self.mouse_over_in_last_30_secs or self.kbd_activity_in_last_30_secs:
+ if not self.mouse_over_in_last_30_secs or \
+ self.kbd_activity_in_last_30_secs:
self.send_chatstate('inactive', contact)
- # assume no activity and let the motion-notify or 'insert-text' make them True
- # refresh 30 seconds too or else it's 30 - 5 = 25 seconds!
+ # assume no activity and let the motion-notify or 'insert-text' make them
+ # True refresh 30 seconds too or else it's 30 - 5 = 25 seconds!
self.reset_kbd_mouse_timeout_vars()
return True # loop forever
@@ -1201,7 +1212,7 @@ class ChatControl(ChatControlBase):
self.kbd_activity_in_last_30_secs = False
def print_conversation(self, text, frm = '', tim = None,
- encrypted = False, subject = None):
+ encrypted = False, subject = None, xhtml = None):
'''Print a line in the conversation:
if contact is set to status: it's a status message
if contact is set to another value: it's an outgoing message
@@ -1241,7 +1252,7 @@ class ChatControl(ChatControlBase):
kind = 'outgoing'
name = gajim.nicks[self.account]
ChatControlBase.print_conversation_line(self, text, kind, name, tim,
- subject = subject, old_kind = self.old_msg_kind)
+ subject = subject, old_kind = self.old_msg_kind, xhtml = xhtml)
if text.startswith('/me ') or text.startswith('/me\n'):
self.old_msg_kind = None
else:
@@ -1459,18 +1470,20 @@ class ChatControl(ChatControlBase):
# prevent going paused if we we were not composing (JEP violation)
if state == 'paused' and not contact.our_chatstate == 'composing':
- MessageControl.send_message(self, None, chatstate = 'active') # go active before
+ # go active before
+ MessageControl.send_message(self, None, chatstate = 'active')
contact.our_chatstate = 'active'
self.reset_kbd_mouse_timeout_vars()
# if we're inactive prevent composing (JEP violation)
elif contact.our_chatstate == 'inactive' and state == 'composing':
- MessageControl.send_message(self, None, chatstate = 'active') # go active before
+ # go active before
+ MessageControl.send_message(self, None, chatstate = 'active')
contact.our_chatstate = 'active'
self.reset_kbd_mouse_timeout_vars()
- MessageControl.send_message(self, None, chatstate = state, msg_id = contact.msg_id,
- composing_jep = contact.composing_jep)
+ MessageControl.send_message(self, None, chatstate = state,
+ msg_id = contact.msg_id, composing_jep = contact.composing_jep)
contact.our_chatstate = state
if contact.our_chatstate == 'active':
self.reset_kbd_mouse_timeout_vars()
diff --git a/src/common/connection.py b/src/common/connection.py
index 2f4e0895a..803fca602 100644
--- a/src/common/connection.py
+++ b/src/common/connection.py
@@ -34,6 +34,8 @@ from common import GnuPG
from connection_handlers import *
USE_GPG = GnuPG.USE_GPG
+from rst_xhtml_generator import create_xhtml
+
class Connection(ConnectionHandlers):
'''Connection class'''
def __init__(self, name):
@@ -650,6 +652,7 @@ class Connection(ConnectionHandlers):
p.setTag(common.xmpp.NS_SIGNED + ' x').setData(signed)
if self.connection:
self.connection.send(p)
+ self.priority = priority
self.dispatch('STATUS', show)
def _on_disconnected(self):
@@ -660,15 +663,18 @@ class Connection(ConnectionHandlers):
def get_status(self):
return STATUS_LIST[self.connected]
- def send_motd(self, jid, subject = '', msg = ''):
+
+ def send_motd(self, jid, subject = '', msg = '', xhtml = None):
if not self.connection:
return
- msg_iq = common.xmpp.Message(to = jid, body = msg, subject = subject)
+ msg_iq = common.xmpp.Message(to = jid, body = msg, subject = subject,
+ xhtml = xhtml)
+
self.connection.send(msg_iq)
def send_message(self, jid, msg, keyID, type = 'chat', subject='',
chatstate = None, msg_id = None, composing_jep = None, resource = None,
- user_nick = None):
+ user_nick = None, xhtml = None):
if not self.connection:
return
if not msg and chatstate is None:
@@ -684,18 +690,20 @@ class Connection(ConnectionHandlers):
if msgenc:
msgtxt = '[This message is encrypted]'
lang = os.getenv('LANG')
- if lang is not None or lang != 'en': # we're not english
- msgtxt = _('[This message is encrypted]') +\
- ' ([This message is encrypted])' # one in locale and one en
+ if lang is not None and lang != 'en': # we're not english
+ # one in locale and one en
+ msgtxt = _('[This message is *encrypted* (See :JEP:`27`]') +\
+ ' ([This message is *encrypted* (See :JEP:`27`])'
if type == 'chat':
- msg_iq = common.xmpp.Message(to = fjid, body = msgtxt, typ = type)
+ msg_iq = common.xmpp.Message(to = fjid, body = msgtxt, typ = type,
+ xhtml = xhtml)
else:
if subject:
msg_iq = common.xmpp.Message(to = fjid, body = msgtxt,
- typ = 'normal', subject = subject)
+ typ = 'normal', subject = subject, xhtml = xhtml)
else:
msg_iq = common.xmpp.Message(to = fjid, body = msgtxt,
- typ = 'normal')
+ typ = 'normal', xhtml = xhtml)
if msgenc:
msg_iq.setTag(common.xmpp.NS_ENCRYPTED + ' x').setData(msgenc)
@@ -713,7 +721,8 @@ class Connection(ConnectionHandlers):
msg_iq.setTag(chatstate, namespace = common.xmpp.NS_CHATSTATES)
if composing_jep == 'JEP-0022' or not composing_jep:
# JEP-0022
- chatstate_node = msg_iq.setTag('x', namespace = common.xmpp.NS_EVENT)
+ chatstate_node = msg_iq.setTag('x',
+ namespace = common.xmpp.NS_EVENT)
if not msgtxt: # when no
, add
if not msg_id: # avoid putting 'None' in tag
msg_id = ''
@@ -975,10 +984,10 @@ class Connection(ConnectionHandlers):
last_log = 0
self.last_history_line[jid]= last_log
- def send_gc_message(self, jid, msg):
+ def send_gc_message(self, jid, msg, xhtml = None):
if not self.connection:
return
- msg_iq = common.xmpp.Message(jid, msg, typ = 'groupchat')
+ msg_iq = common.xmpp.Message(jid, msg, typ = 'groupchat', xhtml = xhtml)
self.connection.send(msg_iq)
self.dispatch('MSGSENT', (jid, msg))
@@ -1104,8 +1113,8 @@ class Connection(ConnectionHandlers):
self.connection.send(iq)
def unregister_account(self, on_remove_success):
- # no need to write this as a class method and keep the value of on_remove_success
- # as a class property as pass it as an argument
+ # no need to write this as a class method and keep the value of
+ # on_remove_success as a class property as pass it as an argument
def _on_unregister_account_connect(con):
self.on_connect_auth = None
if gajim.account_is_connected(self.name):
diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py
index 411bfd251..cd4f0c8ef 100644
--- a/src/common/connection_handlers.py
+++ b/src/common/connection_handlers.py
@@ -1317,6 +1317,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco)
def _messageCB(self, con, msg):
'''Called when we receive a message'''
msgtxt = msg.getBody()
+ msghtml = msg.getXHTML()
mtype = msg.getType()
subject = msg.getSubject() # if not there, it's None
tim = msg.getTimestamp()
@@ -1402,7 +1403,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco)
has_timestamp = False
if msg.timestamp:
has_timestamp = True
- self.dispatch('GC_MSG', (frm, msgtxt, tim, has_timestamp))
+ self.dispatch('GC_MSG', (frm, msgtxt, tim, has_timestamp, msghtml))
if self.name not in no_log_for and not int(float(time.mktime(tim))) <= \
self.last_history_line[jid] and msgtxt:
gajim.logger.write('gc_msg', frm, msgtxt, tim = tim)
@@ -1414,7 +1415,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco)
msg_id = gajim.logger.write('chat_msg_recv', frm, msgtxt, tim = tim,
subject = subject)
self.dispatch('MSG', (frm, msgtxt, tim, encrypted, mtype, subject,
- chatstate, msg_id, composing_jep, user_nick))
+ chatstate, msg_id, composing_jep, user_nick, msghtml))
else: # it's single message
if self.name not in no_log_for and jid not in no_log_for and msgtxt:
gajim.logger.write('single_msg_recv', frm, msgtxt, tim = tim,
@@ -1428,7 +1429,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco)
self.dispatch('GC_INVITATION',(frm, jid_from, reason, password))
else:
self.dispatch('MSG', (frm, msgtxt, tim, encrypted, 'normal',
- subject, chatstate, msg_id, composing_jep, user_nick))
+ subject, chatstate, msg_id, composing_jep, user_nick, msghtml))
# END messageCB
def _presenceCB(self, con, prs):
@@ -1445,8 +1446,8 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco)
# one
who = str(prs.getFrom())
jid_stripped, resource = gajim.get_room_and_nick_from_fjid(who)
- self.dispatch('GC_MSG', (jid_stripped, _('Nickname not allowed: %s') % \
- resource, None, False))
+ self.dispatch('GC_MSG', (jid_stripped,
+ _('Nickname not allowed: %s') % resource, None, False, None))
return
jid_stripped, resource = gajim.get_room_and_nick_from_fjid(who)
timestamp = None
diff --git a/src/common/xmpp/protocol.py b/src/common/xmpp/protocol.py
index fee4cdb0c..daf309773 100644
--- a/src/common/xmpp/protocol.py
+++ b/src/common/xmpp/protocol.py
@@ -397,10 +397,18 @@ class Message(Protocol):
def getBody(self):
""" Returns text of the message. """
return self.getTagData('body')
- def getXHTML(self):
- """ Returns serialized xhtml-im body text of the message. """
+ def getXHTML(self, xmllang=None):
+ """ Returns serialized xhtml-im element text of the message.
+
+ TODO: Returning a DOM could make rendering faster."""
xhtml = self.getTag('html')
- return str(xhtml.getTag('body'))
+ if xhtml:
+ if xmllang:
+ body = xhtml.getTag('body', attrs={'xml:lang':xmllang})
+ else:
+ body = xhtml.getTag('body')
+ return str(body)
+ return None
def getSubject(self):
""" Returns subject of the message. """
return self.getTagData('subject')
@@ -410,11 +418,22 @@ class Message(Protocol):
def setBody(self,val):
""" Sets the text of the message. """
self.setTagData('body',val)
- def setXHTML(self,val):
+
+ def setXHTML(self,val,xmllang=None):
""" Sets the xhtml text of the message (JEP-0071).
The parameter is the "inner html" to the body."""
- dom = NodeBuilder(val)
- self.setTag('html',namespace=NS_XHTML_IM).setTag('body',namespace=NS_XHTML).addChild(node=dom.getDom())
+ try:
+ if xmllang:
+ dom = NodeBuilder('' + val + '').getDom()
+ else:
+ dom = NodeBuilder(''+val+'',0).getDom()
+ if self.getTag('html'):
+ self.getTag('html').addChild(node=dom)
+ else:
+ self.setTag('html',namespace=NS_XHTML_IM).addChild(node=dom)
+ except Exception, e:
+ print "Error", e
+ pass #FIXME: log. we could not set xhtml (parse error, whatever)
def setSubject(self,val):
""" Sets the subject of the message. """
self.setTagData('subject',val)
diff --git a/src/conversation_textview.py b/src/conversation_textview.py
index dcc4c2159..18e907a6f 100644
--- a/src/conversation_textview.py
+++ b/src/conversation_textview.py
@@ -39,12 +39,16 @@ from common import helpers
from calendar import timegm
from common.fuzzyclock import FuzzyClock
+from htmltextview import HtmlTextView
+
+
class ConversationTextview:
'''Class for the conversation textview (where user reads already said messages)
for chat/groupchat windows'''
def __init__(self, account):
# no need to inherit TextView, use it as property is safer
- self.tv = gtk.TextView()
+ self.tv = HtmlTextView()
+ self.tv.html_hyperlink_handler = self.html_hyperlink_handler
# set properties
self.tv.set_border_width(1)
@@ -98,7 +102,7 @@ class ConversationTextview:
tag.set_property('weight', pango.WEIGHT_BOLD)
tag = buffer.create_tag('time_sometimes')
- tag.set_property('foreground', 'grey')
+ tag.set_property('foreground', 'darkgrey')
tag.set_property('scale', pango.SCALE_SMALL)
tag.set_property('justification', gtk.JUSTIFY_CENTER)
@@ -141,6 +145,8 @@ class ConversationTextview:
path_to_file = os.path.join(gajim.DATA_DIR, 'pixmaps', 'muc_separator.png')
self.focus_out_line_pixbuf = gtk.gdk.pixbuf_new_from_file(path_to_file)
+ # use it for hr too
+ self.tv.focus_out_line_pixbuf = self.focus_out_line_pixbuf
def del_handlers(self):
for i in self.handlers.keys():
@@ -504,6 +510,15 @@ class ConversationTextview:
# we launch the correct application
helpers.launch_browser_mailer(kind, word)
+ def html_hyperlink_handler(self, texttag, widget, event, iter, kind, href):
+ if event.type == gtk.gdk.BUTTON_PRESS:
+ if event.button == 3: # right click
+ self.make_link_menu(event, kind, href)
+ else:
+ # we launch the correct application
+ helpers.launch_browser_mailer(kind, href)
+
+
def detect_and_print_special_text(self, otext, other_tags):
'''detects special text (emots & links & formatting)
prints normal text before any special text it founts,
@@ -637,11 +652,11 @@ class ConversationTextview:
def print_empty_line(self):
buffer = self.tv.get_buffer()
end_iter = buffer.get_end_iter()
- buffer.insert(end_iter, '\n')
+ buffer.insert_with_tags_by_name(end_iter, '\n', 'eol')
def print_conversation_line(self, text, jid, kind, name, tim,
- other_tags_for_name = [], other_tags_for_time = [],
- other_tags_for_text = [], subject = None, old_kind = None):
+ other_tags_for_name = [], other_tags_for_time = [], other_tags_for_text = [],
+ subject = None, old_kind = None, xhtml = None):
'''prints 'chat' type messages'''
buffer = self.tv.get_buffer()
buffer.begin_user_action()
@@ -651,7 +666,7 @@ class ConversationTextview:
at_the_end = True
if buffer.get_char_count() > 0:
- buffer.insert(end_iter, '\n')
+ buffer.insert_with_tags_by_name(end_iter, '\n', 'eol')
if kind == 'incoming_queue':
kind = 'incoming'
if old_kind == 'incoming_queue':
@@ -726,7 +741,7 @@ class ConversationTextview:
else:
self.print_name(name, kind, other_tags_for_name)
self.print_subject(subject)
- self.print_real_text(text, text_tags, name)
+ self.print_real_text(text, text_tags, name, xhtml)
# scroll to the end of the textview
if at_the_end or kind == 'outgoing':
@@ -763,8 +778,18 @@ class ConversationTextview:
buffer.insert(end_iter, subject)
self.print_empty_line()
- def print_real_text(self, text, text_tags = [], name = None):
+ def print_real_text(self, text, text_tags = [], name = None, xhtml = None):
'''this adds normal and special text. call this to add text'''
+ if xhtml:
+ try:
+ if name and (text.startswith('/me ') or text.startswith('/me\n')):
+ xhtml = xhtml.replace('/me', '%s'% (name,), 1)
+ self.tv.display_html(xhtml.encode('utf-8'))
+ return
+ except Exception, e:
+ gajim.log.debug(str("Error processing xhtml")+str(e))
+ gajim.log.debug(str("with |"+xhtml+"|"))
+
buffer = self.tv.get_buffer()
# /me is replaced by name if name is given
if name and (text.startswith('/me ') or text.startswith('/me\n')):
diff --git a/src/gajim.py b/src/gajim.py
index f7971ff55..1dd62b35f 100755
--- a/src/gajim.py
+++ b/src/gajim.py
@@ -508,13 +508,14 @@ class Interface:
def handle_event_msg(self, account, array):
# 'MSG' (account, (jid, msg, time, encrypted, msg_type, subject,
- # chatstate, msg_id, composing_jep, user_nick)) user_nick is JEP-0172
+ # chatstate, msg_id, composing_jep, user_nick, xhtml)) user_nick is JEP-0172
full_jid_with_resource = array[0]
jid = gajim.get_jid_without_resource(full_jid_with_resource)
resource = gajim.get_resource_from_jid(full_jid_with_resource)
message = array[1]
+ encrypted = array[3]
msg_type = array[4]
subject = array[5]
chatstate = array[6]
@@ -598,18 +599,26 @@ class Interface:
if pm:
nickname = resource
msg_type = 'pm'
- groupchat_control.on_private_message(nickname, message, array[2])
+ groupchat_control.on_private_message(nickname, message, array[2],
+ array[10])
else:
# array: (jid, msg, time, encrypted, msg_type, subject)
- self.roster.on_message(jid, message, array[2], account, array[3],
- msg_type, subject, resource, msg_id, array[9], advanced_notif_num)
+ if encrypted:
+ self.roster.on_message(jid, message, array[2], account, array[3],
+ msg_type, subject, resource, msg_id, array[9],
+ advanced_notif_num)
+ else:
+ #xhtml in last element
+ self.roster.on_message(jid, message, array[2], account, array[3],
+ msg_type, subject, resource, msg_id, array[9],
+ advanced_notif_num, xhtml = array[10])
nickname = gajim.get_name_from_jid(account, jid)
# Check and do wanted notifications
msg = message
if subject:
msg = _('Subject: %s') % subject + '\n' + msg
- notify.notify('new_message', full_jid_with_resource, account, [msg_type, first, nickname,
- msg], advanced_notif_num)
+ notify.notify('new_message', full_jid_with_resource, account, [msg_type,
+ first, nickname, msg], advanced_notif_num)
if self.remote_ctrl:
self.remote_ctrl.raise_signal('NewMessage', (account, array))
@@ -699,8 +708,8 @@ class Interface:
self.remote_ctrl.raise_signal('Subscribed', (account, array))
def handle_event_unsubscribed(self, account, jid):
- dialogs.InformationDialog(_('Contact "%s" removed subscription from you') % jid,
- _('You will always see him or her as offline.'))
+ dialogs.InformationDialog(_('Contact "%s" removed subscription from you')\
+ % jid, _('You will always see him or her as offline.'))
# FIXME: Per RFC 3921, we can "deny" ack as well, but the GUI does not show deny
gajim.connections[account].ack_unsubscribed(jid)
if self.remote_ctrl:
@@ -743,8 +752,8 @@ class Interface:
config.ServiceRegistrationWindow(array[0], array[1], account,
array[2])
else:
- dialogs.ErrorDialog(_('Contact with "%s" cannot be established')\
-% array[0], _('Check your connection or try again later.'))
+ dialogs.ErrorDialog(_('Contact with "%s" cannot be established') \
+ % array[0], _('Check your connection or try again later.'))
def handle_event_agent_info_items(self, account, array):
#('AGENT_INFO_ITEMS', account, (agent, node, items))
@@ -878,8 +887,8 @@ class Interface:
# Get the window and control for the updated status, this may be a PrivateChatControl
control = self.msg_win_mgr.get_control(room_jid, account)
if control:
- control.chg_contact_status(nick, show, status, array[4], array[5], array[6],
- array[7], array[8], array[9], array[10])
+ control.chg_contact_status(nick, show, status, array[4], array[5],
+ array[6], array[7], array[8], array[9], array[10])
# print status in chat window and update status/GPG image
if self.msg_win_mgr.has_window(fjid, account):
@@ -897,7 +906,7 @@ class Interface:
def handle_event_gc_msg(self, account, array):
- # ('GC_MSG', account, (jid, msg, time, has_timestamp))
+ # ('GC_MSG', account, (jid, msg, time, has_timestamp, htmlmsg))
jids = array[0].split('/', 1)
room_jid = jids[0]
gc_control = self.msg_win_mgr.get_control(room_jid, account)
@@ -909,7 +918,7 @@ class Interface:
else:
# message from someone
nick = jids[1]
- gc_control.on_message(nick, array[1], array[2], array[3])
+ gc_control.on_message(nick, array[1], array[2], array[3], array[4])
if self.remote_ctrl:
self.remote_ctrl.raise_signal('GCMessage', (account, array))
@@ -926,7 +935,8 @@ class Interface:
gc_control.print_conversation(array[2])
# ... Or the message comes from the occupant who set the subject
elif len(jids) > 1:
- gc_control.print_conversation('%s has set the subject to %s' % (jids[1], array[1]))
+ gc_control.print_conversation('%s has set the subject to %s' % (
+ jids[1], array[1]))
def handle_event_gc_config(self, account, array):
#('GC_CONFIG', account, (jid, config)) config is a dict
diff --git a/src/groupchat_control.py b/src/groupchat_control.py
index 339a3b8a1..3590163d9 100644
--- a/src/groupchat_control.py
+++ b/src/groupchat_control.py
@@ -103,9 +103,10 @@ class PrivateChatControl(ChatControl):
if not message:
return
- # We need to make sure that we can still send through the room and that the
- # recipient did not go away
- contact = gajim.contacts.get_first_contact_from_jid(self.account, self.contact.jid)
+ # We need to make sure that we can still send through the room and that
+ # the recipient did not go away
+ contact = gajim.contacts.get_first_contact_from_jid(self.account,
+ self.contact.jid)
if contact is None:
# contact was from pm in MUC
room, nick = gajim.get_room_and_nick_from_fjid(self.contact.jid)
@@ -384,7 +385,8 @@ class GroupchatControl(ChatControlBase):
color = self.parent_win.notebook.style.fg[gtk.STATE_ACTIVE]
elif chatstate == 'newmsg' and (not has_focus or not current_tab) and\
not self.attention_flag:
- color_name = gajim.config.get_per('themes', theme, 'state_muc_msg_color')
+ color_name = gajim.config.get_per('themes', theme,
+ 'state_muc_msg_color')
if color_name:
color = gtk.gdk.colormap_get_system().alloc_color(color_name)
@@ -433,18 +435,18 @@ class GroupchatControl(ChatControlBase):
childs[3].set_sensitive(False)
return menu
- def on_message(self, nick, msg, tim, has_timestamp = False):
+ def on_message(self, nick, msg, tim, has_timestamp = False, xhtml = None):
if not nick:
# message from server
- self.print_conversation(msg, tim = tim)
+ self.print_conversation(msg, tim = tim, xhtml = xhtml)
else:
# message from someone
if has_timestamp:
- self.print_old_conversation(msg, nick, tim)
+ self.print_old_conversation(msg, nick, tim, xhtml)
else:
- self.print_conversation(msg, nick, tim)
+ self.print_conversation(msg, nick, tim, xhtml)
- def on_private_message(self, nick, msg, tim):
+ def on_private_message(self, nick, msg, tim, xhtml):
# Do we have a queue?
fjid = self.room_jid + '/' + nick
no_queue = len(gajim.events.get_events(self.account, fjid)) == 0
@@ -452,7 +454,7 @@ class GroupchatControl(ChatControlBase):
# We print if window is opened
pm_control = gajim.interface.msg_win_mgr.get_control(fjid, self.account)
if pm_control:
- pm_control.print_conversation(msg, tim = tim)
+ pm_control.print_conversation(msg, tim = tim, xhtml = xhtml)
return
event = gajim.events.create_event('pm', (msg, '', 'incoming', tim,
@@ -505,7 +507,7 @@ class GroupchatControl(ChatControlBase):
gc_count_nicknames_colors = 0
gc_custom_colors = {}
- def print_old_conversation(self, text, contact, tim = None):
+ def print_old_conversation(self, text, contact, tim = None, xhtml = None):
if isinstance(text, str):
text = unicode(text, 'utf-8')
if contact == self.nick: # it's us
@@ -518,9 +520,9 @@ class GroupchatControl(ChatControlBase):
small_attr = []
ChatControlBase.print_conversation_line(self, text, kind, contact, tim,
small_attr, small_attr + ['restored_message'],
- small_attr + ['restored_message'])
+ small_attr + ['restored_message'], xhtml = xhtml)
- def print_conversation(self, text, contact = '', tim = None):
+ def print_conversation(self, text, contact = '', tim = None, xhtml = None):
'''Print a line in the conversation:
if contact is set: it's a message from someone or an info message (contact
= 'info' in such a case)
@@ -574,7 +576,7 @@ class GroupchatControl(ChatControlBase):
self.check_and_possibly_add_focus_out_line()
ChatControlBase.print_conversation_line(self, text, kind, contact, tim,
- other_tags_for_name, [], other_tags_for_text)
+ other_tags_for_name, [], other_tags_for_text, xhtml = xhtml)
def get_nb_unread(self):
nb = len(gajim.events.get_events(self.account, self.room_jid,
@@ -643,13 +645,16 @@ class GroupchatControl(ChatControlBase):
word[char_position:char_position+1]
if (refer_to_nick_char != ''):
refer_to_nick_char_code = ord(refer_to_nick_char)
- if ((refer_to_nick_char_code < 65 or refer_to_nick_char_code > 123)\
- or (refer_to_nick_char_code < 97 and refer_to_nick_char_code > 90)):
+ if ((refer_to_nick_char_code < 65 or \
+ refer_to_nick_char_code > 123) or \
+ (refer_to_nick_char_code < 97 and \
+ refer_to_nick_char_code > 90)):
return True
else:
- # This is A->Z or a->z, we can be sure our nick is the beginning
- # of a real word, do not highlight. Note that we can probably
- # do a better detection of non-punctuation characters
+ # This is A->Z or a->z, we can be sure our nick is the
+ # beginning of a real word, do not highlight. Note that we
+ # can probably do a better detection of non-punctuation
+ # characters
return False
else: # Special word == word, no char after in word
return True
@@ -698,7 +703,8 @@ class GroupchatControl(ChatControlBase):
gc_contact.affiliation, gc_contact.status,
gc_contact.jid)
- def on_send_pm(self, widget=None, model=None, iter=None, nick=None, msg=None):
+ def on_send_pm(self, widget = None, model = None, iter = None, nick = None,
+ msg = None):
'''opens a chat window and msg is not None sends private message to a
contact in a room'''
if nick is None:
@@ -707,14 +713,16 @@ class GroupchatControl(ChatControlBase):
self._start_private_message(nick)
if msg:
- gajim.interface.msg_win_mgr.get_control(fjid, self.account).send_message(msg)
+ gajim.interface.msg_win_mgr.get_control(fjid, self.account).\
+ send_message(msg)
def draw_contact(self, nick, selected=False, focus=False):
iter = self.get_contact_iter(nick)
if not iter:
return
model = self.list_treeview.get_model()
- gc_contact = gajim.contacts.get_gc_contact(self.account, self.room_jid, nick)
+ gc_contact = gajim.contacts.get_gc_contact(self.account, self.room_jid,
+ nick)
state_images = gajim.interface.roster.jabber_state_images['16']
if len(gajim.events.get_events(self.account, self.room_jid + '/' + nick)):
image = state_images['message']
@@ -752,8 +760,8 @@ class GroupchatControl(ChatControlBase):
scaled_pixbuf = None
model[iter][C_AVATAR] = scaled_pixbuf
- def chg_contact_status(self, nick, show, status, role, affiliation, jid, reason, actor,
- statusCode, new_nick):
+ def chg_contact_status(self, nick, show, status, role, affiliation, jid,
+ reason, actor, statusCode, new_nick):
'''When an occupant changes his or her status'''
if show == 'invisible':
return
@@ -845,7 +853,8 @@ class GroupchatControl(ChatControlBase):
self.add_contact_to_roster(nick, show, role,
affiliation, status, jid)
else:
- c = gajim.contacts.get_gc_contact(self.account, self.room_jid, nick)
+ c = gajim.contacts.get_gc_contact(self.account, self.room_jid,
+ nick)
if c.show == show and c.status == status and \
c.affiliation == affiliation: #no change
return
@@ -948,7 +957,8 @@ class GroupchatControl(ChatControlBase):
iter = self.get_contact_iter(nick)
if not iter:
return
- gc_contact = gajim.contacts.get_gc_contact(self.account, self.room_jid, nick)
+ gc_contact = gajim.contacts.get_gc_contact(self.account, self.room_jid,
+ nick)
if gc_contact:
gajim.contacts.remove_gc_contact(self.account, gc_contact)
parent_iter = model.iter_parent(iter)
@@ -1096,7 +1106,8 @@ class GroupchatControl(ChatControlBase):
if len(message_array):
message_array = message_array[0].split()
nick = message_array.pop(0)
- room_nicks = gajim.contacts.get_nick_list(self.account, self.room_jid)
+ room_nicks = gajim.contacts.get_nick_list(self.account,
+ self.room_jid)
reason = ' '.join(message_array)
if nick in room_nicks:
ban_jid = gajim.construct_fjid(self.room_jid, nick)
@@ -1117,7 +1128,8 @@ class GroupchatControl(ChatControlBase):
if len(message_array):
message_array = message_array[0].split()
nick = message_array.pop(0)
- room_nicks = gajim.contacts.get_nick_list(self.account, self.room_jid)
+ room_nicks = gajim.contacts.get_nick_list(self.account,
+ self.room_jid)
reason = ' '.join(message_array)
if nick in room_nicks:
gajim.connections[self.account].gc_set_role(self.room_jid, nick,
@@ -1177,7 +1189,8 @@ class GroupchatControl(ChatControlBase):
if not self._process_command(message):
# Send the message
- gajim.connections[self.account].send_gc_message(self.room_jid, message)
+ gajim.connections[self.account].send_gc_message(self.room_jid,
+ message)
self.msg_textview.get_buffer().set_text('')
self.msg_textview.grab_focus()
@@ -1200,7 +1213,8 @@ class GroupchatControl(ChatControlBase):
self.print_conversation(_('Usage: /%s [reason], closes the current '
'window or tab, displaying reason if specified.') % command, 'info')
elif command == 'compact':
- self.print_conversation(_('Usage: /%s, hide the chat buttons.') % command, 'info')
+ self.print_conversation(_('Usage: /%s, hide the chat buttons.') % \
+ command, 'info')
elif command == 'invite':
self.print_conversation(_('Usage: /%s [reason], invites JID to '
'the current room, optionally providing a reason.') % command,
@@ -1241,7 +1255,8 @@ class GroupchatControl(ChatControlBase):
self.print_conversation(_('No help info for /%s') % command, 'info')
def get_role(self, nick):
- gc_contact = gajim.contacts.get_gc_contact(self.account, self.room_jid, nick)
+ gc_contact = gajim.contacts.get_gc_contact(self.account, self.room_jid,
+ nick)
if gc_contact:
return gc_contact.role
else:
@@ -1327,7 +1342,8 @@ class GroupchatControl(ChatControlBase):
_('Please specify the new subject:'), self.subject)
response = instance.get_response()
if response == gtk.RESPONSE_OK:
- # Note, we don't update self.subject since we don't know whether it will work yet
+ # Note, we don't update self.subject since we don't know whether it
+ # will work yet
subject = instance.input_entry.get_text().decode('utf-8')
gajim.connections[self.account].send_gc_subject(self.room_jid, subject)
@@ -1372,7 +1388,8 @@ class GroupchatControl(ChatControlBase):
_('Bookmark has been added successfully'),
_('You can manage your bookmarks via Actions menu in your roster.'))
- def handle_message_textview_mykey_press(self, widget, event_keyval, event_keymod):
+ def handle_message_textview_mykey_press(self, widget, event_keyval,
+ event_keymod):
# NOTE: handles mykeypress which is custom signal connected to this
# CB in new_room(). for this singal see message_textview.py
@@ -1384,12 +1401,14 @@ class GroupchatControl(ChatControlBase):
message_buffer = widget.get_buffer()
start_iter, end_iter = message_buffer.get_bounds()
- message = message_buffer.get_text(start_iter, end_iter, False).decode('utf-8')
+ message = message_buffer.get_text(start_iter, end_iter, False).decode(
+ 'utf-8')
if event.keyval == gtk.keysyms.Tab: # TAB
cursor_position = message_buffer.get_insert()
end_iter = message_buffer.get_iter_at_mark(cursor_position)
- text = message_buffer.get_text(start_iter, end_iter, False).decode('utf-8')
+ text = message_buffer.get_text(start_iter, end_iter, False).decode(
+ 'utf-8')
if text.endswith(' '):
if not self.last_key_tabs:
return False
@@ -1505,8 +1524,8 @@ class GroupchatControl(ChatControlBase):
# looking for user's affiliation and role
user_nick = self.nick
- user_affiliation = gajim.contacts.get_gc_contact(self.account, self.room_jid,
- user_nick).affiliation
+ user_affiliation = gajim.contacts.get_gc_contact(self.account,
+ self.room_jid, user_nick).affiliation
user_role = self.get_role(user_nick)
# making menu from glade
@@ -1515,9 +1534,10 @@ class GroupchatControl(ChatControlBase):
# these conditions were taken from JEP 0045
item = xml.get_widget('kick_menuitem')
if user_role != 'moderator' or \
- (user_affiliation == 'admin' and target_affiliation == 'owner') or \
- (user_affiliation == 'member' and target_affiliation in ('admin', 'owner')) or \
- (user_affiliation == 'none' and target_affiliation != 'none'):
+ (user_affiliation == 'admin' and target_affiliation == 'owner') or \
+ (user_affiliation == 'member' and target_affiliation in ('admin',
+ 'owner')) or (user_affiliation == 'none' and target_affiliation != \
+ 'none'):
item.set_sensitive(False)
id = item.connect('activate', self.kick, nick)
self.handlers[id] = item
@@ -1525,18 +1545,18 @@ class GroupchatControl(ChatControlBase):
item = xml.get_widget('voice_checkmenuitem')
item.set_active(target_role != 'visitor')
if user_role != 'moderator' or \
- user_affiliation == 'none' or \
- (user_affiliation=='member' and target_affiliation!='none') or \
- target_affiliation in ('admin', 'owner'):
+ user_affiliation == 'none' or \
+ (user_affiliation=='member' and target_affiliation!='none') or \
+ target_affiliation in ('admin', 'owner'):
item.set_sensitive(False)
id = item.connect('activate', self.on_voice_checkmenuitem_activate,
- nick)
+ nick)
self.handlers[id] = item
item = xml.get_widget('moderator_checkmenuitem')
item.set_active(target_role == 'moderator')
if not user_affiliation in ('admin', 'owner') or \
- target_affiliation in ('admin', 'owner'):
+ target_affiliation in ('admin', 'owner'):
item.set_sensitive(False)
id = item.connect('activate', self.on_moderator_checkmenuitem_activate,
nick)
@@ -1544,8 +1564,8 @@ class GroupchatControl(ChatControlBase):
item = xml.get_widget('ban_menuitem')
if not user_affiliation in ('admin', 'owner') or \
- (target_affiliation in ('admin', 'owner') and\
- user_affiliation != 'owner'):
+ (target_affiliation in ('admin', 'owner') and\
+ user_affiliation != 'owner'):
item.set_sensitive(False)
id = item.connect('activate', self.ban, jid)
self.handlers[id] = item
@@ -1553,7 +1573,7 @@ class GroupchatControl(ChatControlBase):
item = xml.get_widget('member_checkmenuitem')
item.set_active(target_affiliation != 'none')
if not user_affiliation in ('admin', 'owner') or \
- (user_affiliation != 'owner' and target_affiliation in ('admin','owner')):
+ (user_affiliation != 'owner' and target_affiliation in ('admin','owner')):
item.set_sensitive(False)
id = item.connect('activate', self.on_member_checkmenuitem_activate,
jid)
@@ -1743,19 +1763,23 @@ class GroupchatControl(ChatControlBase):
def grant_voice(self, widget, nick):
'''grant voice privilege to a user'''
- gajim.connections[self.account].gc_set_role(self.room_jid, nick, 'participant')
+ gajim.connections[self.account].gc_set_role(self.room_jid, nick,
+ 'participant')
def revoke_voice(self, widget, nick):
'''revoke voice privilege to a user'''
- gajim.connections[self.account].gc_set_role(self.room_jid, nick, 'visitor')
+ gajim.connections[self.account].gc_set_role(self.room_jid, nick,
+ 'visitor')
def grant_moderator(self, widget, nick):
'''grant moderator privilege to a user'''
- gajim.connections[self.account].gc_set_role(self.room_jid, nick, 'moderator')
+ gajim.connections[self.account].gc_set_role(self.room_jid, nick,
+ 'moderator')
def revoke_moderator(self, widget, nick):
'''revoke moderator privilege to a user'''
- gajim.connections[self.account].gc_set_role(self.room_jid, nick, 'participant')
+ gajim.connections[self.account].gc_set_role(self.room_jid, nick,
+ 'participant')
def ban(self, widget, jid):
'''ban a user'''
@@ -1770,17 +1794,17 @@ class GroupchatControl(ChatControlBase):
else:
return # stop banning procedure
gajim.connections[self.account].gc_set_affiliation(self.room_jid, jid,
- 'outcast', reason)
+ 'outcast', reason)
def grant_membership(self, widget, jid):
'''grant membership privilege to a user'''
gajim.connections[self.account].gc_set_affiliation(self.room_jid, jid,
- 'member')
+ 'member')
def revoke_membership(self, widget, jid):
'''revoke membership privilege to a user'''
gajim.connections[self.account].gc_set_affiliation(self.room_jid, jid,
- 'none')
+ 'none')
def grant_admin(self, widget, jid):
'''grant administrative privilege to a user'''
@@ -1804,7 +1828,8 @@ class GroupchatControl(ChatControlBase):
c = gajim.contacts.get_gc_contact(self.account, self.room_jid, nick)
c2 = gajim.contacts.contact_from_gc_contact(c)
if gajim.interface.instances[self.account]['infos'].has_key(c2.jid):
- gajim.interface.instances[self.account]['infos'][c2.jid].window.present()
+ gajim.interface.instances[self.account]['infos'][c2.jid].window.\
+ present()
else:
gajim.interface.instances[self.account]['infos'][c2.jid] = \
vcard.VcardWindow(c2, self.account, is_fake = True)
diff --git a/src/htmltextview.py b/src/htmltextview.py
new file mode 100644
index 000000000..4a57a2adb
--- /dev/null
+++ b/src/htmltextview.py
@@ -0,0 +1,968 @@
+### Copyright (C) 2005 Gustavo J. A. M. Carneiro
+### Copyright (C) 2006 Santiago Gala
+###
+### This library is free software; you can redistribute it and/or
+### modify it under the terms of the GNU Lesser General Public
+### License as published by the Free Software Foundation; either
+### version 2 of the License, or (at your option) any later version.
+###
+### This library 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
+### Lesser General Public License for more details.
+###
+### You should have received a copy of the GNU Lesser General Public
+### License along with this library; if not, write to the
+### Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+### Boston, MA 02111-1307, USA.
+
+
+"""
+A gtk.TextView-based renderer for XHTML-IM, as described in:
+ http://www.jabber.org/jeps/jep-0071.html
+
+Starting with the version posted by Gustavo Carneiro,
+I (Santiago Gala) am trying to make it more compatible
+with the markup that docutils generate, and also more
+modular.
+
+"""
+
+import gobject
+import pango
+import gtk
+import xml.sax, xml.sax.handler
+import re
+import warnings
+from cStringIO import StringIO
+import urllib2
+import operator
+
+#from common import i18n
+
+
+import tooltips
+
+
+__all__ = ['HtmlTextView']
+
+whitespace_rx = re.compile("\\s+")
+allwhitespace_rx = re.compile("^\\s*$")
+
+## pixels = points * display_resolution
+display_resolution = 0.3514598*(gtk.gdk.screen_height() /
+ float(gtk.gdk.screen_height_mm()))
+
+#embryo of CSS classes
+classes = {
+ #'system-message':';display: none',
+ 'problematic':';color: red',
+}
+
+#styles for elemens
+element_styles = {
+ 'u' : ';text-decoration: underline',
+ 'em' : ';font-style: oblique',
+ 'cite' : '; background-color:rgb(170,190,250); font-style: oblique',
+ 'li' : '; margin-left: 1em; margin-right: 10%',
+ 'strong' : ';font-weight: bold',
+ 'pre' : '; background-color:rgb(190,190,190); font-family: monospace; white-space: pre; margin-left: 1em; margin-right: 10%',
+ 'kbd' : ';background-color:rgb(210,210,210);font-family: monospace',
+ 'blockquote': '; background-color:rgb(170,190,250); margin-left: 2em; margin-right: 10%',
+ 'dt' : ';font-weight: bold; font-style: oblique',
+ 'dd' : ';margin-left: 2em; font-style: oblique'
+}
+# no difference for the moment
+element_styles['dfn'] = element_styles['em']
+element_styles['var'] = element_styles['em']
+# deprecated, legacy, presentational
+element_styles['tt'] = element_styles['kbd']
+element_styles['i'] = element_styles['em']
+element_styles['b'] = element_styles['strong']
+
+class_styles = {
+}
+
+"""
+==========
+ JEP-0071
+==========
+
+This Integration Set includes a subset of the modules defined for
+XHTML 1.0 but does not redefine any existing modules, nor
+does it define any new modules. Specifically, it includes the
+following modules only:
+
+- Structure
+- Text
+
+ * Block
+
+ phrasal
+ addr, blockquote, pre
+ Struc
+ div,p
+ Heading
+ h1, h2, h3, h4, h5, h6
+
+ * Inline
+
+ phrasal
+ abbr, acronym, cite, code, dfn, em, kbd, q, samp, strong, var
+ structural
+ br, span
+
+- Hypertext (a)
+- List (ul, ol, dl)
+- Image (img)
+- Style Attribute
+
+Therefore XHTML-IM uses the following content models:
+
+ Block.mix
+ Block-like elements, e.g., paragraphs
+ Flow.mix
+ Any block or inline elements
+ Inline.mix
+ Character-level elements
+ InlineNoAnchor.class
+ Anchor element
+ InlinePre.mix
+ Pre element
+
+XHTML-IM also uses the following Attribute Groups:
+
+Core.extra.attrib
+ TBD
+I18n.extra.attrib
+ TBD
+Common.extra
+ style
+
+
+...
+#block level:
+#Heading h
+# ( pres = h1 | h2 | h3 | h4 | h5 | h6 )
+#Block ( phrasal = address | blockquote | pre )
+#NOT ( presentational = hr )
+# ( structural = div | p )
+#other: section
+#Inline ( phrasal = abbr | acronym | cite | code | dfn | em | kbd | q | samp | strong | var )
+#NOT ( presentational = b | big | i | small | sub | sup | tt )
+# ( structural = br | span )
+#Param/Legacy param, font, basefont, center, s, strike, u, dir, menu, isindex
+#
+"""
+
+BLOCK_HEAD = set(( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', ))
+BLOCK_PHRASAL = set(( 'address', 'blockquote', 'pre', ))
+BLOCK_PRES = set(( 'hr', )) #not in xhtml-im
+BLOCK_STRUCT = set(( 'div', 'p', ))
+BLOCK_HACKS = set(( 'table', 'tr' ))
+BLOCK = BLOCK_HEAD.union(BLOCK_PHRASAL).union(BLOCK_STRUCT).union(BLOCK_PRES).union(BLOCK_HACKS)
+
+INLINE_PHRASAL = set('abbr, acronym, cite, code, dfn, em, kbd, q, samp, strong, var'.split(', '))
+INLINE_PRES = set('b, i, u, tt'.split(', ')) #not in xhtml-im
+INLINE_STRUCT = set('br, span'.split(', '))
+INLINE = INLINE_PHRASAL.union(INLINE_PRES).union(INLINE_STRUCT)
+
+LIST_ELEMS = set( 'dl, ol, ul'.split(', '))
+
+for name in BLOCK_HEAD:
+ num = eval(name[1])
+ size = (num-1) // 2
+ weigth = (num - 1) % 2
+ element_styles[name] = '; font-size: %s; %s' % ( ('large', 'medium', 'small')[size],
+ ('font-weight: bold', 'font-style: oblique')[weigth],
+ )
+
+
+def build_patterns(view, config, interface):
+ #extra, rst does not mark _underline_ or /it/ up
+ #actually , or are not in the JEP-0071, but are seen in the wild
+ basic_pattern = r'(? gtk.gdk.Color'''
+ if color.startswith("rgb(") and color.endswith(')'):
+ r, g, b = [int(c)*257 for c in color[4:-1].split(',')]
+ return gtk.gdk.Color(r, g, b)
+ else:
+ return gtk.gdk.color_parse(color)
+
+
+class HtmlHandler(xml.sax.handler.ContentHandler):
+
+ def __init__(self, textview, startiter):
+ xml.sax.handler.ContentHandler.__init__(self)
+ self.textbuf = textview.get_buffer()
+ self.textview = textview
+ self.iter = startiter
+ self.text = ''
+ self.starting=True
+ self.preserve = False
+ self.styles = [] # a gtk.TextTag or None, for each span level
+ self.list_counters = [] # stack (top at head) of list
+ # counters, or None for unordered list
+
+ def _parse_style_color(self, tag, value):
+ color = _parse_css_color(value)
+ tag.set_property("foreground-gdk", color)
+
+ def _parse_style_background_color(self, tag, value):
+ color = _parse_css_color(value)
+ tag.set_property("background-gdk", color)
+ if gtk.gtk_version >= (2, 8):
+ tag.set_property("paragraph-background-gdk", color)
+
+
+ if gtk.gtk_version >= (2, 8, 5) or gobject.pygtk_version >= (2, 8, 1):
+
+ def _get_current_attributes(self):
+ attrs = self.textview.get_default_attributes()
+ self.iter.backward_char()
+ self.iter.get_attributes(attrs)
+ self.iter.forward_char()
+ return attrs
+
+ else:
+
+ ## Workaround http://bugzilla.gnome.org/show_bug.cgi?id=317455
+ def _get_current_style_attr(self, propname, comb_oper=None):
+ tags = [tag for tag in self.styles if tag is not None]
+ tags.reverse()
+ is_set_name = propname + "-set"
+ value = None
+ for tag in tags:
+ if tag.get_property(is_set_name):
+ if value is None:
+ value = tag.get_property(propname)
+ if comb_oper is None:
+ return value
+ else:
+ value = comb_oper(value, tag.get_property(propname))
+ return value
+
+ class _FakeAttrs(object):
+ __slots__ = ("font", "font_scale")
+
+ def _get_current_attributes(self):
+ attrs = self._FakeAttrs()
+ attrs.font_scale = self._get_current_style_attr("scale",
+ operator.mul)
+ if attrs.font_scale is None:
+ attrs.font_scale = 1.0
+ attrs.font = self._get_current_style_attr("font-desc")
+ if attrs.font is None:
+ attrs.font = self.textview.style.font_desc
+ return attrs
+
+
+ def __parse_length_frac_size_allocate(self, textview, allocation,
+ frac, callback, args):
+ callback(allocation.width*frac, *args)
+
+ def _parse_length(self, value, font_relative, callback, *args):
+ '''Parse/calc length, converting to pixels, calls callback(length, *args)
+ when the length is first computed or changes'''
+ if value.endswith('%'):
+ frac = float(value[:-1])/100
+ if font_relative:
+ attrs = self._get_current_attributes()
+ font_size = attrs.font.get_size() / pango.SCALE
+ callback(frac*display_resolution*font_size, *args)
+ else:
+ ## CSS says "Percentage values: refer to width of the closest
+ ## block-level ancestor"
+ ## This is difficult/impossible to implement, so we use
+ ## textview width instead; a reasonable approximation..
+ alloc = self.textview.get_allocation()
+ self.__parse_length_frac_size_allocate(self.textview, alloc,
+ frac, callback, args)
+ self.textview.connect("size-allocate",
+ self.__parse_length_frac_size_allocate,
+ frac, callback, args)
+
+ elif value.endswith('pt'): # points
+ callback(float(value[:-2])*display_resolution, *args)
+
+ elif value.endswith('em'): # ems, the height of the element's font
+ attrs = self._get_current_attributes()
+ font_size = attrs.font.get_size() / pango.SCALE
+ callback(float(value[:-2])*display_resolution*font_size, *args)
+
+ elif value.endswith('ex'): # x-height, ~ the height of the letter 'x'
+ ## FIXME: figure out how to calculate this correctly
+ ## for now 'em' size is used as approximation
+ attrs = self._get_current_attributes()
+ font_size = attrs.font.get_size() / pango.SCALE
+ callback(float(value[:-2])*display_resolution*font_size, *args)
+
+ elif value.endswith('px'): # pixels
+ callback(int(value[:-2]), *args)
+
+ else:
+ warnings.warn("Unable to parse length value '%s'" % value)
+
+ def __parse_font_size_cb(length, tag):
+ tag.set_property("size-points", length/display_resolution)
+ __parse_font_size_cb = staticmethod(__parse_font_size_cb)
+
+ def _parse_style_display(self, tag, value):
+ if value == 'none':
+ tag.set_property('invisible','true')
+ #Fixme: display: block, inline
+
+ def _parse_style_font_size(self, tag, value):
+ try:
+ scale = {
+ "xx-small": pango.SCALE_XX_SMALL,
+ "x-small": pango.SCALE_X_SMALL,
+ "small": pango.SCALE_SMALL,
+ "medium": pango.SCALE_MEDIUM,
+ "large": pango.SCALE_LARGE,
+ "x-large": pango.SCALE_X_LARGE,
+ "xx-large": pango.SCALE_XX_LARGE,
+ } [value]
+ except KeyError:
+ pass
+ else:
+ attrs = self._get_current_attributes()
+ tag.set_property("scale", scale / attrs.font_scale)
+ return
+ if value == 'smaller':
+ tag.set_property("scale", pango.SCALE_SMALL)
+ return
+ if value == 'larger':
+ tag.set_property("scale", pango.SCALE_LARGE)
+ return
+ self._parse_length(value, True, self.__parse_font_size_cb, tag)
+
+ def _parse_style_font_style(self, tag, value):
+ try:
+ style = {
+ "normal": pango.STYLE_NORMAL,
+ "italic": pango.STYLE_ITALIC,
+ "oblique": pango.STYLE_OBLIQUE,
+ } [value]
+ except KeyError:
+ warnings.warn("unknown font-style %s" % value)
+ else:
+ tag.set_property("style", style)
+
+ def __frac_length_tag_cb(self,length, tag, propname):
+ styles = self._get_style_tags()
+ if styles:
+ length += styles[-1].get_property(propname)
+ tag.set_property(propname, length)
+ #__frac_length_tag_cb = staticmethod(__frac_length_tag_cb)
+
+ def _parse_style_margin_left(self, tag, value):
+ self._parse_length(value, False, self.__frac_length_tag_cb,
+ tag, "left-margin")
+
+ def _parse_style_margin_right(self, tag, value):
+ self._parse_length(value, False, self.__frac_length_tag_cb,
+ tag, "right-margin")
+
+ def _parse_style_font_weight(self, tag, value):
+ ## TODO: missing 'bolder' and 'lighter'
+ try:
+ weight = {
+ '100': pango.WEIGHT_ULTRALIGHT,
+ '200': pango.WEIGHT_ULTRALIGHT,
+ '300': pango.WEIGHT_LIGHT,
+ '400': pango.WEIGHT_NORMAL,
+ '500': pango.WEIGHT_NORMAL,
+ '600': pango.WEIGHT_BOLD,
+ '700': pango.WEIGHT_BOLD,
+ '800': pango.WEIGHT_ULTRABOLD,
+ '900': pango.WEIGHT_HEAVY,
+ 'normal': pango.WEIGHT_NORMAL,
+ 'bold': pango.WEIGHT_BOLD,
+ } [value]
+ except KeyError:
+ warnings.warn("unknown font-style %s" % value)
+ else:
+ tag.set_property("weight", weight)
+
+ def _parse_style_font_family(self, tag, value):
+ tag.set_property("family", value)
+
+ def _parse_style_text_align(self, tag, value):
+ try:
+ align = {
+ 'left': gtk.JUSTIFY_LEFT,
+ 'right': gtk.JUSTIFY_RIGHT,
+ 'center': gtk.JUSTIFY_CENTER,
+ 'justify': gtk.JUSTIFY_FILL,
+ } [value]
+ except KeyError:
+ warnings.warn("Invalid text-align:%s requested" % value)
+ else:
+ tag.set_property("justification", align)
+
+ def _parse_style_text_decoration(self, tag, value):
+ if value == "none":
+ tag.set_property("underline", pango.UNDERLINE_NONE)
+ tag.set_property("strikethrough", False)
+ elif value == "underline":
+ tag.set_property("underline", pango.UNDERLINE_SINGLE)
+ tag.set_property("strikethrough", False)
+ elif value == "overline":
+ warnings.warn("text-decoration:overline not implemented")
+ tag.set_property("underline", pango.UNDERLINE_NONE)
+ tag.set_property("strikethrough", False)
+ elif value == "line-through":
+ tag.set_property("underline", pango.UNDERLINE_NONE)
+ tag.set_property("strikethrough", True)
+ elif value == "blink":
+ warnings.warn("text-decoration:blink not implemented")
+ else:
+ warnings.warn("text-decoration:%s not implemented" % value)
+
+ def _parse_style_white_space(self, tag, value):
+ if value == 'pre':
+ tag.set_property("wrap_mode", gtk.WRAP_NONE)
+ elif value == 'normal':
+ tag.set_property("wrap_mode", gtk.WRAP_WORD)
+ elif value == 'nowrap':
+ tag.set_property("wrap_mode", gtk.WRAP_NONE)
+
+
+ ## build a dictionary mapping styles to methods, for greater speed
+ __style_methods = dict()
+ for style in ["background-color", "color", "font-family", "font-size",
+ "font-style", "font-weight", "margin-left", "margin-right",
+ "text-align", "text-decoration", "white-space", 'display' ]:
+ try:
+ method = locals()["_parse_style_%s" % style.replace('-', '_')]
+ except KeyError:
+ warnings.warn("Style attribute '%s' not yet implemented" % style)
+ else:
+ __style_methods[style] = method
+ del style
+ ## --
+
+ def _get_style_tags(self):
+ return [tag for tag in self.styles if tag is not None]
+
+ def _create_url(self, href, title, type_, id_):
+ tag = self.textbuf.create_tag(id_)
+ if href and href[0] != '#':
+ tag.href = href
+ tag.type_ = type_ # to be used by the URL handler
+ tag.connect('event', self.textview.html_hyperlink_handler, 'url', href)
+ tag.set_property('foreground', '#0000ff')
+ tag.set_property('underline', pango.UNDERLINE_SINGLE)
+ tag.is_anchor = True
+ if title:
+ tag.title = title
+ return tag
+
+
+ def _begin_span(self, style, tag=None, id_=None):
+ if style is None:
+ self.styles.append(tag)
+ return None
+ if tag is None:
+ if id_:
+ tag = self.textbuf.create_tag(id_)
+ else:
+ tag = self.textbuf.create_tag()
+ for attr, val in [item.split(':', 1) for item in style.split(';') if len(item.strip())]:
+ attr = attr.strip().lower()
+ val = val.strip()
+ try:
+ method = self.__style_methods[attr]
+ except KeyError:
+ warnings.warn("Style attribute '%s' requested "
+ "but not yet implemented" % attr)
+ else:
+ method(self, tag, val)
+ self.styles.append(tag)
+
+ def _end_span(self):
+ self.styles.pop()
+
+ def _jump_line(self):
+ self.textbuf.insert_with_tags_by_name(self.iter, '\n', 'eol')
+ self.starting = True
+
+ def _insert_text(self, text):
+ if self.starting and text != '\n':
+ self.starting = (text[-1] == '\n')
+ tags = self._get_style_tags()
+ if tags:
+ self.textbuf.insert_with_tags(self.iter, text, *tags)
+ else:
+ self.textbuf.insert(self.iter, text)
+
+ def _starts_line(self):
+ return self.starting or self.iter.starts_line()
+
+ def _flush_text(self):
+ if not self.text: return
+ text, self.text = self.text, ''
+ if not self.preserve:
+ text = text.replace('\n', ' ')
+ self.handle_specials(whitespace_rx.sub(' ', text))
+ else:
+ self._insert_text(text.strip("\n"))
+
+ def _anchor_event(self, tag, textview, event, iter, href, type_):
+ if event.type == gtk.gdk.BUTTON_PRESS:
+ self.textview.emit("url-clicked", href, type_)
+ return True
+ return False
+
+ def handle_specials(self, text):
+ index = 0
+ se = self.textview.config.get('show_ascii_formatting_chars')
+ if self.textview.config.get('emoticons_theme'):
+ iterator = self.textview.emot_and_basic_re.finditer(text)
+ else:
+ iterator = self.textview.basic_pattern_re.finditer(text)
+ for match in iterator:
+ start, end = match.span()
+ special_text = text[start:end]
+ if start != 0:
+ self._insert_text(text[index:start])
+ index = end # update index
+ #emoticons
+ possible_emot_ascii_caps = special_text.upper() # emoticons keys are CAPS
+ if self.textview.config.get('emoticons_theme') and \
+ possible_emot_ascii_caps in self.textview.interface.emoticons.keys():
+ #it's an emoticon
+ emot_ascii = possible_emot_ascii_caps
+ anchor = self.textbuf.create_child_anchor(self.iter)
+ img = gtk.Image()
+ img.set_from_file(self.textview.interface.emoticons[emot_ascii])
+ # TODO: add alt/tooltip with the special_text (a11y)
+ self.textview.add_child_at_anchor(img, anchor)
+ img.show()
+ else:
+ # now print it
+ if special_text.startswith('/'): # it's explicit italics
+ self.startElement('i', {})
+ elif special_text.startswith('_'): # it's explicit underline
+ self.startElement("u", {})
+ if se: self._insert_text(special_text[0])
+ self.handle_specials(special_text[1:-1])
+ if se: self._insert_text(special_text[0])
+ if special_text.startswith('_'): # it's explicit underline
+ self.endElement('u')
+ if special_text.startswith('/'): # it's explicit italics
+ self.endElement('i')
+ self._insert_text(text[index:])
+
+ def characters(self, content):
+ if self.preserve:
+ self.text += content
+ return
+ if allwhitespace_rx.match(content) is not None and self._starts_line():
+ return
+ self.text += content
+ #if self.text: self.text += ' '
+ #self.handle_specials(whitespace_rx.sub(' ', content))
+ if allwhitespace_rx.match(self.text) is not None and self._starts_line():
+ self.text = ''
+ #self._flush_text()
+
+
+ def startElement(self, name, attrs):
+ self._flush_text()
+ klass = [i for i in attrs.get('class',' ').split(' ') if i]
+ style = attrs.get('style','')
+ #Add styles defined for classes
+ #TODO: priority between class and style elements?
+ for k in klass:
+ if k in classes:
+ style += classes[k]
+
+ tag = None
+ #FIXME: if we want to use id, it needs to be unique across
+ # the whole textview, so we need to add something like the
+ # message-id to it.
+ #id_ = attrs.get('id',None)
+ id_ = None
+ if name == 'a':
+ #TODO: accesskey, charset, hreflang, rel, rev, tabindex, type
+ href = attrs.get('href', None)
+ title = attrs.get('title', attrs.get('rel',href))
+ type_ = attrs.get('type', None)
+ tag = self._create_url(href, title, type_, id_)
+ elif name == 'blockquote':
+ cite = attrs.get('cite', None)
+ if cite:
+ tag = self.textbuf.create_tag(id_)
+ tag.title = title
+ tag.is_anchor = True
+ elif name in LIST_ELEMS:
+ style += ';margin-left: 2em'
+ if name in element_styles:
+ style += element_styles[name]
+
+ if style == '':
+ style = None
+ self._begin_span(style, tag, id_)
+
+ if name == 'br':
+ pass # handled in endElement
+ elif name == 'hr':
+ pass # handled in endElement
+ elif name in BLOCK:
+ if not self._starts_line():
+ self._jump_line()
+ if name == 'pre':
+ self.preserve = True
+ elif name == 'span':
+ pass
+ elif name in ('dl', 'ul'):
+ if not self._starts_line():
+ self._jump_line()
+ self.list_counters.append(None)
+ elif name == 'ol':
+ if not self._starts_line():
+ self._jump_line()
+ self.list_counters.append(0)
+ elif name == 'li':
+ if self.list_counters[-1] is None:
+ li_head = unichr(0x2022)
+ else:
+ self.list_counters[-1] += 1
+ li_head = "%i." % self.list_counters[-1]
+ self.text = ' '*len(self.list_counters)*4 + li_head + ' '
+ self._flush_text()
+ self.starting = True
+ elif name == 'dd':
+ self._jump_line()
+ elif name == 'dt':
+ if not self.starting:
+ self._jump_line()
+ elif name == 'img':
+ try:
+ ## Max image size = 10 MB (to try to prevent DoS)
+ mem = urllib2.urlopen(attrs['src']).read(10*1024*1024)
+ ## Caveat: GdkPixbuf is known not to be safe to load
+ ## images from network... this program is now potentially
+ ## hackable ;)
+ loader = gtk.gdk.PixbufLoader()
+ loader.write(mem); loader.close()
+ pixbuf = loader.get_pixbuf()
+ except Exception, ex:
+ gajim.log.debug(str('Error loading image'+ex))
+ pixbuf = None
+ alt = attrs.get('alt', "Broken image")
+ try:
+ loader.close()
+ except: pass
+ if pixbuf is not None:
+ tags = self._get_style_tags()
+ if tags:
+ tmpmark = self.textbuf.create_mark(None, self.iter, True)
+
+ self.textbuf.insert_pixbuf(self.iter, pixbuf)
+
+ if tags:
+ start = self.textbuf.get_iter_at_mark(tmpmark)
+ for tag in tags:
+ self.textbuf.apply_tag(tag, start, self.iter)
+ self.textbuf.delete_mark(tmpmark)
+ else:
+ self._insert_text("[IMG: %s]" % alt)
+ elif name == 'body' or name == 'html':
+ pass
+ elif name == 'a':
+ pass
+ elif name in INLINE:
+ pass
+ else:
+ warnings.warn("Unhandled element '%s'" % name)
+
+ def endElement(self, name):
+ endPreserving = False
+ newLine = False
+ if name == 'br':
+ newLine = True
+ elif name == 'hr':
+ #FIXME: plenty of unused attributes (width, height,...) :)
+ self._jump_line()
+ try:
+ self.textbuf.insert_pixbuf(self.iter, self.textview.focus_out_line_pixbuf)
+ #self._insert_text(u"\u2550"*40)
+ self._jump_line()
+ except Exception, e:
+ log.debug(str("Error in hr"+e))
+ elif name in LIST_ELEMS:
+ self.list_counters.pop()
+ elif name == 'li':
+ newLine = True
+ elif name == 'img':
+ pass
+ elif name == 'body' or name == 'html':
+ pass
+ elif name == 'a':
+ pass
+ elif name in INLINE:
+ pass
+ elif name in ('dd', 'dt', ):
+ pass
+ elif name in BLOCK:
+ if name == 'pre':
+ endPreserving = True
+ else:
+ warnings.warn("Unhandled element '%s'" % name)
+ self._flush_text()
+ if endPreserving:
+ self.preserve = False
+ if newLine:
+ self._jump_line()
+ self._end_span()
+ #if not self._starts_line():
+ # self.text = ' '
+
+class HtmlTextView(gtk.TextView):
+ __gtype_name__ = 'HtmlTextView'
+ __gsignals__ = {
+ 'url-clicked': (gobject.SIGNAL_RUN_LAST, None, (str, str)), # href, type
+ }
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+ self.set_wrap_mode(gtk.WRAP_CHAR)
+ self.set_editable(False)
+ self._changed_cursor = False
+ self.connect("motion-notify-event", self.__motion_notify_event)
+ self.connect("leave-notify-event", self.__leave_event)
+ self.connect("enter-notify-event", self.__motion_notify_event)
+ self.get_buffer().create_tag('eol', scale = pango.SCALE_XX_SMALL)
+ self.tooltip = tooltips.BaseTooltip()
+ # needed to avoid bootstrapping problems
+ from common import gajim
+ self.config = gajim.config
+ self.interface = gajim.interface
+ self.log = gajim.log
+ # end big hack
+ build_patterns(self,gajim.config,gajim.interface)
+
+ def __leave_event(self, widget, event):
+ if self._changed_cursor:
+ window = widget.get_window(gtk.TEXT_WINDOW_TEXT)
+ window.set_cursor(gtk.gdk.Cursor(gtk.gdk.XTERM))
+ self._changed_cursor = False
+
+ def show_tooltip(self, tag):
+ if not self.tooltip.win:
+ # check if the current pointer is still over the line
+ text = getattr(tag, 'title', False)
+ if text:
+ pointer = self.get_pointer()
+ position = self.window.get_origin()
+ win = self.get_toplevel()
+ self.tooltip.show_tooltip(text, 8, position[1] + pointer[1])
+
+ def __motion_notify_event(self, widget, event):
+ x, y, _ = widget.window.get_pointer()
+ x, y = widget.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, x, y)
+ tags = widget.get_iter_at_location(x, y).get_tags()
+ is_over_anchor = False
+ for tag in tags:
+ if getattr(tag, 'is_anchor', False):
+ is_over_anchor = True
+ break
+ if self.tooltip.timeout != 0:
+ # Check if we should hide the line tooltip
+ if not is_over_anchor:
+ self.tooltip.hide_tooltip()
+ if not self._changed_cursor and is_over_anchor:
+ window = widget.get_window(gtk.TEXT_WINDOW_TEXT)
+ window.set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND2))
+ self._changed_cursor = True
+ gobject.timeout_add(500,
+ self.show_tooltip, tag)
+ elif self._changed_cursor and not is_over_anchor:
+ window = widget.get_window(gtk.TEXT_WINDOW_TEXT)
+ window.set_cursor(gtk.gdk.Cursor(gtk.gdk.XTERM))
+ self._changed_cursor = False
+ return False
+
+ def display_html(self, html):
+ buffer = self.get_buffer()
+ eob = buffer.get_end_iter()
+ ## this works too if libxml2 is not available
+ # parser = xml.sax.make_parser(['drv_libxml2'])
+ # parser.setFeature(xml.sax.handler.feature_validation, True)
+ parser = xml.sax.make_parser()
+ parser.setContentHandler(HtmlHandler(self, eob))
+ parser.parse(StringIO(html))
+
+ #if not eob.starts_line():
+ # buffer.insert(eob, "\n")
+
+if gobject.pygtk_version < (2, 8):
+ gobject.type_register(HtmlTextView)
+
+change_cursor = None
+
+if __name__ == '__main__':
+
+ htmlview = HtmlTextView()
+
+ tooltip = tooltips.BaseTooltip()
+ def on_textview_motion_notify_event(widget, event):
+ '''change the cursor to a hand when we are over a mail or an url'''
+ global change_cursor
+ pointer_x, pointer_y, spam = htmlview.window.get_pointer()
+ x, y = htmlview.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, pointer_x,
+ pointer_y)
+ tags = htmlview.get_iter_at_location(x, y).get_tags()
+ if change_cursor:
+ htmlview.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
+ gtk.gdk.Cursor(gtk.gdk.XTERM))
+ change_cursor = None
+ tag_table = htmlview.get_buffer().get_tag_table()
+ over_line = False
+ for tag in tags:
+ try:
+ if tag.is_anchor:
+ htmlview.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
+ gtk.gdk.Cursor(gtk.gdk.HAND2))
+ change_cursor = tag
+ elif tag == tag_table.lookup('focus-out-line'):
+ over_line = True
+ except: pass
+
+ #if line_tooltip.timeout != 0:
+ # Check if we should hide the line tooltip
+ # if not over_line:
+ # line_tooltip.hide_tooltip()
+ #if over_line and not line_tooltip.win:
+ # line_tooltip.timeout = gobject.timeout_add(500,
+ # show_line_tooltip)
+ # htmlview.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
+ # gtk.gdk.Cursor(gtk.gdk.LEFT_PTR))
+ # change_cursor = tag
+
+ htmlview.connect('motion_notify_event', on_textview_motion_notify_event)
+
+ def handler(texttag, widget, event, iter, kind, href):
+ if event.type == gtk.gdk.BUTTON_PRESS:
+ print href
+
+ htmlview.html_hyperlink_handler = handler
+
+ htmlview.display_html('Hello\n'
+ '
\n'
+ '
World\n'
+ '
\n')
+ htmlview.display_html("
")
+ htmlview.display_html("""
+
+ OMG,
+ I'm green
+ with envy!
+
+ """)
+ htmlview.display_html("
")
+ htmlview.display_html("""
+
+ As Emerson said in his essay Self-Reliance:
+
+ "A foolish consistency is the hobgoblin of little minds."
+
+
+ """)
+ htmlview.display_html("
")
+ htmlview.display_html("""
+
+ Hey, are you licensed to Jabber?
+
+
+ """)
+ htmlview.display_html("
")
+ htmlview.display_html("""
+
+
def fac(n):
+ def faciter(n,acc):
+ if n==0: return acc
+ return faciter(n-1, acc*n)
+ if n<0: raise ValueError("Must be non-negative")
+ return faciter(n,1)
+
+ """)
+ htmlview.display_html("
")
+ htmlview.display_html("""
+
+
+ - One
+ - Two is nested:
+ - Three
+
+ """)
+ htmlview.show()
+ sw = gtk.ScrolledWindow()
+ sw.set_property("hscrollbar-policy", gtk.POLICY_AUTOMATIC)
+ sw.set_property("vscrollbar-policy", gtk.POLICY_AUTOMATIC)
+ sw.set_property("border-width", 0)
+ sw.add(htmlview)
+ sw.show()
+ frame = gtk.Frame()
+ frame.set_shadow_type(gtk.SHADOW_IN)
+ frame.show()
+ frame.add(sw)
+ w = gtk.Window()
+ w.add(frame)
+ w.set_default_size(400, 300)
+ w.show_all()
+ w.connect("destroy", lambda w: gtk.main_quit())
+ gtk.main()
diff --git a/src/roster_window.py b/src/roster_window.py
index b2268e921..2f5f08083 100644
--- a/src/roster_window.py
+++ b/src/roster_window.py
@@ -2531,7 +2531,7 @@ _('If "%s" accepts this request you will know his or her status.') % jid)
def on_message(self, jid, msg, tim, account, encrypted = False,
msg_type = '', subject = None, resource = '', msg_id = None,
- user_nick = '', advanced_notif_num = None):
+ user_nick = '', advanced_notif_num = None, xhtml = None):
'''when we receive a message'''
contact = None
# if chat window will be for specific resource
@@ -2599,7 +2599,7 @@ _('If "%s" accepts this request you will know his or her status.') % jid)
if msg_type == 'error':
typ = 'status'
ctrl.print_conversation(msg, typ, tim = tim, encrypted = encrypted,
- subject = subject)
+ subject = subject, xhtml = xhtml)
if msg_id:
gajim.logger.set_read_messages([msg_id])
return
@@ -2613,7 +2613,7 @@ _('If "%s" accepts this request you will know his or her status.') % jid)
show_in_roster = notify.get_show_in_roster(event_type, account, contact)
show_in_systray = notify.get_show_in_systray(event_type, account, contact)
event = gajim.events.create_event(type_, (msg, subject, msg_type, tim,
- encrypted, resource, msg_id), show_in_roster = show_in_roster,
+ encrypted, resource, msg_id, xhtml), show_in_roster = show_in_roster,
show_in_systray = show_in_systray)
gajim.events.add_event(account, fjid, event)
if popup: