[Santiago Gala] we can now see XHTML (JEP 0071). See #316

This commit is contained in:
Yann Leboulanger 2006-10-03 14:12:42 +00:00
parent aeb6116ba6
commit 6b40b5ad32
9 changed files with 1202 additions and 132 deletions

View file

@ -424,7 +424,8 @@ class ChatControlBase(MessageControl):
message_textview = widget message_textview = widget
message_buffer = message_textview.get_buffer() message_buffer = message_textview.get_buffer()
start_iter, end_iter = message_buffer.get_bounds() 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 # construct event instance from binding
event = gtk.gdk.Event(gtk.gdk.KEY_PRESS) # it's always a key-press here 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 self.send_message(message) # send the message
else: else:
# Give the control itself a chance to process # 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): def _process_command(self, message):
if not message: if not message:
@ -533,7 +535,7 @@ class ChatControlBase(MessageControl):
def print_conversation_line(self, text, kind, name, tim, def print_conversation_line(self, text, kind, name, tim,
other_tags_for_name = [], other_tags_for_time = [], other_tags_for_name = [], other_tags_for_time = [],
other_tags_for_text = [], count_as_new = True, other_tags_for_text = [], count_as_new = True,
subject = None, old_kind = None): subject = None, old_kind = None, xhtml = None):
'''prints 'chat' type messages''' '''prints 'chat' type messages'''
jid = self.contact.jid jid = self.contact.jid
full_jid = self.get_full_jid() full_jid = self.get_full_jid()
@ -543,7 +545,7 @@ class ChatControlBase(MessageControl):
end = True end = True
textview.print_conversation_line(text, jid, kind, name, tim, textview.print_conversation_line(text, jid, kind, name, tim,
other_tags_for_name, other_tags_for_time, other_tags_for_text, other_tags_for_name, other_tags_for_time, other_tags_for_text,
subject, old_kind) subject, old_kind, xhtml)
if not count_as_new: if not count_as_new:
return return
@ -765,7 +767,8 @@ class ChatControlBase(MessageControl):
#whatever is already typed #whatever is already typed
start_iter = conv_buf.get_start_iter() start_iter = conv_buf.get_start_iter()
end_iter = conv_buf.get_end_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 self.typing_new = False
if direction == 'up': if direction == 'up':
if self.sent_history_pos == 0: if self.sent_history_pos == 0:
@ -825,8 +828,8 @@ class ChatControl(ChatControlBase):
old_msg_kind = None # last kind of the printed message old_msg_kind = None # last kind of the printed message
def __init__(self, parent_win, contact, acct, resource = None): def __init__(self, parent_win, contact, acct, resource = None):
ChatControlBase.__init__(self, self.TYPE_ID, parent_win, 'chat_child_vbox', ChatControlBase.__init__(self, self.TYPE_ID, parent_win,
(_('Chat'), _('Chats')), contact, acct, resource) 'chat_child_vbox', (_('Chat'), _('Chats')), contact, acct, resource)
# for muc use: # for muc use:
# widget = self.xml.get_widget('muc_window_actions_button') # 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) id = widget.connect('clicked', self.on_actions_button_clicked)
self.handlers[id] = widget 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.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 # Initialize drag-n-drop
self.TARGET_TYPE_URI_LIST = 80 self.TARGET_TYPE_URI_LIST = 80
self.dnd_list = [ ( 'text/uri-list', 0, self.TARGET_TYPE_URI_LIST ) ] 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.handlers[id] = self.widget
self.widget.drag_dest_set(gtk.DEST_DEFAULT_MOTION | self.widget.drag_dest_set(gtk.DEST_DEFAULT_MOTION |
gtk.DEST_DEFAULT_HIGHLIGHT | gtk.DEST_DEFAULT_HIGHLIGHT |
@ -862,17 +868,21 @@ class ChatControl(ChatControlBase):
self._on_window_motion_notify) self._on_window_motion_notify)
self.handlers[id] = self.parent_win.window self.handlers[id] = self.parent_win.window
message_tv_buffer = self.msg_textview.get_buffer() 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 self.handlers[id] = message_tv_buffer
widget = self.xml.get_widget('avatar_eventbox') 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 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 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 self.handlers[id] = widget
widget = self.xml.get_widget('gpg_togglebutton') widget = self.xml.get_widget('gpg_togglebutton')
@ -1166,8 +1176,8 @@ class ChatControl(ChatControlBase):
if current_state == 'composing': if current_state == 'composing':
self.send_chatstate('paused') # pause composing self.send_chatstate('paused') # pause composing
# assume no activity and let the motion-notify or 'insert-text' make them True # assume no activity and let the motion-notify or 'insert-text' make them
# refresh 30 seconds vars too or else it's 30 - 5 = 25 seconds! # True refresh 30 seconds vars too or else it's 30 - 5 = 25 seconds!
self.reset_kbd_mouse_timeout_vars() self.reset_kbd_mouse_timeout_vars()
return True # loop forever 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: if self.mouse_over_in_last_5_secs or self.kbd_activity_in_last_5_secs:
return True # loop forever 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) self.send_chatstate('inactive', contact)
# assume no activity and let the motion-notify or 'insert-text' make them True # assume no activity and let the motion-notify or 'insert-text' make them
# refresh 30 seconds too or else it's 30 - 5 = 25 seconds! # True refresh 30 seconds too or else it's 30 - 5 = 25 seconds!
self.reset_kbd_mouse_timeout_vars() self.reset_kbd_mouse_timeout_vars()
return True # loop forever return True # loop forever
@ -1201,7 +1212,7 @@ class ChatControl(ChatControlBase):
self.kbd_activity_in_last_30_secs = False self.kbd_activity_in_last_30_secs = False
def print_conversation(self, text, frm = '', tim = None, def print_conversation(self, text, frm = '', tim = None,
encrypted = False, subject = None): encrypted = False, subject = None, xhtml = None):
'''Print a line in the conversation: '''Print a line in the conversation:
if contact is set to status: it's a status message if contact is set to status: it's a status message
if contact is set to another value: it's an outgoing message if contact is set to another value: it's an outgoing message
@ -1241,7 +1252,7 @@ class ChatControl(ChatControlBase):
kind = 'outgoing' kind = 'outgoing'
name = gajim.nicks[self.account] name = gajim.nicks[self.account]
ChatControlBase.print_conversation_line(self, text, kind, name, tim, 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'): if text.startswith('/me ') or text.startswith('/me\n'):
self.old_msg_kind = None self.old_msg_kind = None
else: else:
@ -1459,18 +1470,20 @@ class ChatControl(ChatControlBase):
# prevent going paused if we we were not composing (JEP violation) # prevent going paused if we we were not composing (JEP violation)
if state == 'paused' and not contact.our_chatstate == 'composing': 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' contact.our_chatstate = 'active'
self.reset_kbd_mouse_timeout_vars() self.reset_kbd_mouse_timeout_vars()
# if we're inactive prevent composing (JEP violation) # if we're inactive prevent composing (JEP violation)
elif contact.our_chatstate == 'inactive' and state == 'composing': 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' contact.our_chatstate = 'active'
self.reset_kbd_mouse_timeout_vars() self.reset_kbd_mouse_timeout_vars()
MessageControl.send_message(self, None, chatstate = state, msg_id = contact.msg_id, MessageControl.send_message(self, None, chatstate = state,
composing_jep = contact.composing_jep) msg_id = contact.msg_id, composing_jep = contact.composing_jep)
contact.our_chatstate = state contact.our_chatstate = state
if contact.our_chatstate == 'active': if contact.our_chatstate == 'active':
self.reset_kbd_mouse_timeout_vars() self.reset_kbd_mouse_timeout_vars()

View file

@ -34,6 +34,8 @@ from common import GnuPG
from connection_handlers import * from connection_handlers import *
USE_GPG = GnuPG.USE_GPG USE_GPG = GnuPG.USE_GPG
from rst_xhtml_generator import create_xhtml
class Connection(ConnectionHandlers): class Connection(ConnectionHandlers):
'''Connection class''' '''Connection class'''
def __init__(self, name): def __init__(self, name):
@ -650,6 +652,7 @@ class Connection(ConnectionHandlers):
p.setTag(common.xmpp.NS_SIGNED + ' x').setData(signed) p.setTag(common.xmpp.NS_SIGNED + ' x').setData(signed)
if self.connection: if self.connection:
self.connection.send(p) self.connection.send(p)
self.priority = priority
self.dispatch('STATUS', show) self.dispatch('STATUS', show)
def _on_disconnected(self): def _on_disconnected(self):
@ -660,15 +663,18 @@ class Connection(ConnectionHandlers):
def get_status(self): def get_status(self):
return STATUS_LIST[self.connected] 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: if not self.connection:
return 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) self.connection.send(msg_iq)
def send_message(self, jid, msg, keyID, type = 'chat', subject='', def send_message(self, jid, msg, keyID, type = 'chat', subject='',
chatstate = None, msg_id = None, composing_jep = None, resource = None, chatstate = None, msg_id = None, composing_jep = None, resource = None,
user_nick = None): user_nick = None, xhtml = None):
if not self.connection: if not self.connection:
return return
if not msg and chatstate is None: if not msg and chatstate is None:
@ -684,18 +690,20 @@ class Connection(ConnectionHandlers):
if msgenc: if msgenc:
msgtxt = '[This message is encrypted]' msgtxt = '[This message is encrypted]'
lang = os.getenv('LANG') lang = os.getenv('LANG')
if lang is not None or lang != 'en': # we're not english if lang is not None and lang != 'en': # we're not english
msgtxt = _('[This message is encrypted]') +\ # one in locale and one en
' ([This message is encrypted])' # one in locale and one en msgtxt = _('[This message is *encrypted* (See :JEP:`27`]') +\
' ([This message is *encrypted* (See :JEP:`27`])'
if type == 'chat': 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: else:
if subject: if subject:
msg_iq = common.xmpp.Message(to = fjid, body = msgtxt, msg_iq = common.xmpp.Message(to = fjid, body = msgtxt,
typ = 'normal', subject = subject) typ = 'normal', subject = subject, xhtml = xhtml)
else: else:
msg_iq = common.xmpp.Message(to = fjid, body = msgtxt, msg_iq = common.xmpp.Message(to = fjid, body = msgtxt,
typ = 'normal') typ = 'normal', xhtml = xhtml)
if msgenc: if msgenc:
msg_iq.setTag(common.xmpp.NS_ENCRYPTED + ' x').setData(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) msg_iq.setTag(chatstate, namespace = common.xmpp.NS_CHATSTATES)
if composing_jep == 'JEP-0022' or not composing_jep: if composing_jep == 'JEP-0022' or not composing_jep:
# JEP-0022 # 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 <body>, add <id> if not msgtxt: # when no <body>, add <id>
if not msg_id: # avoid putting 'None' in <id> tag if not msg_id: # avoid putting 'None' in <id> tag
msg_id = '' msg_id = ''
@ -975,10 +984,10 @@ class Connection(ConnectionHandlers):
last_log = 0 last_log = 0
self.last_history_line[jid]= last_log 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: if not self.connection:
return 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.connection.send(msg_iq)
self.dispatch('MSGSENT', (jid, msg)) self.dispatch('MSGSENT', (jid, msg))
@ -1104,8 +1113,8 @@ class Connection(ConnectionHandlers):
self.connection.send(iq) self.connection.send(iq)
def unregister_account(self, on_remove_success): def unregister_account(self, on_remove_success):
# no need to write this as a class method and keep the value of on_remove_success # no need to write this as a class method and keep the value of
# as a class property as pass it as an argument # on_remove_success as a class property as pass it as an argument
def _on_unregister_account_connect(con): def _on_unregister_account_connect(con):
self.on_connect_auth = None self.on_connect_auth = None
if gajim.account_is_connected(self.name): if gajim.account_is_connected(self.name):

View file

@ -1317,6 +1317,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco)
def _messageCB(self, con, msg): def _messageCB(self, con, msg):
'''Called when we receive a message''' '''Called when we receive a message'''
msgtxt = msg.getBody() msgtxt = msg.getBody()
msghtml = msg.getXHTML()
mtype = msg.getType() mtype = msg.getType()
subject = msg.getSubject() # if not there, it's None subject = msg.getSubject() # if not there, it's None
tim = msg.getTimestamp() tim = msg.getTimestamp()
@ -1402,7 +1403,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco)
has_timestamp = False has_timestamp = False
if msg.timestamp: if msg.timestamp:
has_timestamp = True 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))) <= \ if self.name not in no_log_for and not int(float(time.mktime(tim))) <= \
self.last_history_line[jid] and msgtxt: self.last_history_line[jid] and msgtxt:
gajim.logger.write('gc_msg', frm, msgtxt, tim = tim) 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, msg_id = gajim.logger.write('chat_msg_recv', frm, msgtxt, tim = tim,
subject = subject) subject = subject)
self.dispatch('MSG', (frm, msgtxt, tim, encrypted, mtype, 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 else: # it's single message
if self.name not in no_log_for and jid not in no_log_for and msgtxt: 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, 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)) self.dispatch('GC_INVITATION',(frm, jid_from, reason, password))
else: else:
self.dispatch('MSG', (frm, msgtxt, tim, encrypted, 'normal', 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 # END messageCB
def _presenceCB(self, con, prs): def _presenceCB(self, con, prs):
@ -1445,8 +1446,8 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco)
# one # one
who = str(prs.getFrom()) who = str(prs.getFrom())
jid_stripped, resource = gajim.get_room_and_nick_from_fjid(who) jid_stripped, resource = gajim.get_room_and_nick_from_fjid(who)
self.dispatch('GC_MSG', (jid_stripped, _('Nickname not allowed: %s') % \ self.dispatch('GC_MSG', (jid_stripped,
resource, None, False)) _('Nickname not allowed: %s') % resource, None, False, None))
return return
jid_stripped, resource = gajim.get_room_and_nick_from_fjid(who) jid_stripped, resource = gajim.get_room_and_nick_from_fjid(who)
timestamp = None timestamp = None

View file

@ -397,10 +397,18 @@ class Message(Protocol):
def getBody(self): def getBody(self):
""" Returns text of the message. """ """ Returns text of the message. """
return self.getTagData('body') return self.getTagData('body')
def getXHTML(self): def getXHTML(self, xmllang=None):
""" Returns serialized xhtml-im body text of the message. """ """ Returns serialized xhtml-im element text of the message.
TODO: Returning a DOM could make rendering faster."""
xhtml = self.getTag('html') 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): def getSubject(self):
""" Returns subject of the message. """ """ Returns subject of the message. """
return self.getTagData('subject') return self.getTagData('subject')
@ -410,11 +418,22 @@ class Message(Protocol):
def setBody(self,val): def setBody(self,val):
""" Sets the text of the message. """ """ Sets the text of the message. """
self.setTagData('body',val) self.setTagData('body',val)
def setXHTML(self,val):
def setXHTML(self,val,xmllang=None):
""" Sets the xhtml text of the message (JEP-0071). """ Sets the xhtml text of the message (JEP-0071).
The parameter is the "inner html" to the body.""" The parameter is the "inner html" to the body."""
dom = NodeBuilder(val) try:
self.setTag('html',namespace=NS_XHTML_IM).setTag('body',namespace=NS_XHTML).addChild(node=dom.getDom()) if xmllang:
dom = NodeBuilder('<body xmlns="'+NS_XHTML+'" xml:lang="'+xmllang+'" >' + val + '</body>').getDom()
else:
dom = NodeBuilder('<body xmlns="'+NS_XHTML+'">'+val+'</body>',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): def setSubject(self,val):
""" Sets the subject of the message. """ """ Sets the subject of the message. """
self.setTagData('subject',val) self.setTagData('subject',val)

View file

@ -39,12 +39,16 @@ from common import helpers
from calendar import timegm from calendar import timegm
from common.fuzzyclock import FuzzyClock from common.fuzzyclock import FuzzyClock
from htmltextview import HtmlTextView
class ConversationTextview: class ConversationTextview:
'''Class for the conversation textview (where user reads already said messages) '''Class for the conversation textview (where user reads already said messages)
for chat/groupchat windows''' for chat/groupchat windows'''
def __init__(self, account): def __init__(self, account):
# no need to inherit TextView, use it as property is safer # 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 # set properties
self.tv.set_border_width(1) self.tv.set_border_width(1)
@ -98,7 +102,7 @@ class ConversationTextview:
tag.set_property('weight', pango.WEIGHT_BOLD) tag.set_property('weight', pango.WEIGHT_BOLD)
tag = buffer.create_tag('time_sometimes') 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('scale', pango.SCALE_SMALL)
tag.set_property('justification', gtk.JUSTIFY_CENTER) 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') 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) 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): def del_handlers(self):
for i in self.handlers.keys(): for i in self.handlers.keys():
@ -504,6 +510,15 @@ class ConversationTextview:
# we launch the correct application # we launch the correct application
helpers.launch_browser_mailer(kind, word) 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): def detect_and_print_special_text(self, otext, other_tags):
'''detects special text (emots & links & formatting) '''detects special text (emots & links & formatting)
prints normal text before any special text it founts, prints normal text before any special text it founts,
@ -637,11 +652,11 @@ class ConversationTextview:
def print_empty_line(self): def print_empty_line(self):
buffer = self.tv.get_buffer() buffer = self.tv.get_buffer()
end_iter = buffer.get_end_iter() 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, def print_conversation_line(self, text, jid, kind, name, tim,
other_tags_for_name = [], other_tags_for_time = [], other_tags_for_name = [], other_tags_for_time = [], other_tags_for_text = [],
other_tags_for_text = [], subject = None, old_kind = None): subject = None, old_kind = None, xhtml = None):
'''prints 'chat' type messages''' '''prints 'chat' type messages'''
buffer = self.tv.get_buffer() buffer = self.tv.get_buffer()
buffer.begin_user_action() buffer.begin_user_action()
@ -651,7 +666,7 @@ class ConversationTextview:
at_the_end = True at_the_end = True
if buffer.get_char_count() > 0: 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': if kind == 'incoming_queue':
kind = 'incoming' kind = 'incoming'
if old_kind == 'incoming_queue': if old_kind == 'incoming_queue':
@ -726,7 +741,7 @@ class ConversationTextview:
else: else:
self.print_name(name, kind, other_tags_for_name) self.print_name(name, kind, other_tags_for_name)
self.print_subject(subject) 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 # scroll to the end of the textview
if at_the_end or kind == 'outgoing': if at_the_end or kind == 'outgoing':
@ -763,8 +778,18 @@ class ConversationTextview:
buffer.insert(end_iter, subject) buffer.insert(end_iter, subject)
self.print_empty_line() 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''' '''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', '<dfn>%s</dfn>'% (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() buffer = self.tv.get_buffer()
# /me is replaced by name if name is given # /me is replaced by name if name is given
if name and (text.startswith('/me ') or text.startswith('/me\n')): if name and (text.startswith('/me ') or text.startswith('/me\n')):

View file

@ -508,13 +508,14 @@ class Interface:
def handle_event_msg(self, account, array): def handle_event_msg(self, account, array):
# 'MSG' (account, (jid, msg, time, encrypted, msg_type, subject, # '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] full_jid_with_resource = array[0]
jid = gajim.get_jid_without_resource(full_jid_with_resource) jid = gajim.get_jid_without_resource(full_jid_with_resource)
resource = gajim.get_resource_from_jid(full_jid_with_resource) resource = gajim.get_resource_from_jid(full_jid_with_resource)
message = array[1] message = array[1]
encrypted = array[3]
msg_type = array[4] msg_type = array[4]
subject = array[5] subject = array[5]
chatstate = array[6] chatstate = array[6]
@ -598,18 +599,26 @@ class Interface:
if pm: if pm:
nickname = resource nickname = resource
msg_type = 'pm' msg_type = 'pm'
groupchat_control.on_private_message(nickname, message, array[2]) groupchat_control.on_private_message(nickname, message, array[2],
array[10])
else: else:
# array: (jid, msg, time, encrypted, msg_type, subject) # array: (jid, msg, time, encrypted, msg_type, subject)
if encrypted:
self.roster.on_message(jid, message, array[2], account, array[3], self.roster.on_message(jid, message, array[2], account, array[3],
msg_type, subject, resource, msg_id, array[9], advanced_notif_num) 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) nickname = gajim.get_name_from_jid(account, jid)
# Check and do wanted notifications # Check and do wanted notifications
msg = message msg = message
if subject: if subject:
msg = _('Subject: %s') % subject + '\n' + msg msg = _('Subject: %s') % subject + '\n' + msg
notify.notify('new_message', full_jid_with_resource, account, [msg_type, first, nickname, notify.notify('new_message', full_jid_with_resource, account, [msg_type,
msg], advanced_notif_num) first, nickname, msg], advanced_notif_num)
if self.remote_ctrl: if self.remote_ctrl:
self.remote_ctrl.raise_signal('NewMessage', (account, array)) self.remote_ctrl.raise_signal('NewMessage', (account, array))
@ -699,8 +708,8 @@ class Interface:
self.remote_ctrl.raise_signal('Subscribed', (account, array)) self.remote_ctrl.raise_signal('Subscribed', (account, array))
def handle_event_unsubscribed(self, account, jid): def handle_event_unsubscribed(self, account, jid):
dialogs.InformationDialog(_('Contact "%s" removed subscription from you') % jid, dialogs.InformationDialog(_('Contact "%s" removed subscription from you')\
_('You will always see him or her as offline.')) % 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 # FIXME: Per RFC 3921, we can "deny" ack as well, but the GUI does not show deny
gajim.connections[account].ack_unsubscribed(jid) gajim.connections[account].ack_unsubscribed(jid)
if self.remote_ctrl: if self.remote_ctrl:
@ -878,8 +887,8 @@ class Interface:
# Get the window and control for the updated status, this may be a PrivateChatControl # Get the window and control for the updated status, this may be a PrivateChatControl
control = self.msg_win_mgr.get_control(room_jid, account) control = self.msg_win_mgr.get_control(room_jid, account)
if control: if control:
control.chg_contact_status(nick, show, status, array[4], array[5], array[6], control.chg_contact_status(nick, show, status, array[4], array[5],
array[7], array[8], array[9], array[10]) array[6], array[7], array[8], array[9], array[10])
# print status in chat window and update status/GPG image # print status in chat window and update status/GPG image
if self.msg_win_mgr.has_window(fjid, account): if self.msg_win_mgr.has_window(fjid, account):
@ -897,7 +906,7 @@ class Interface:
def handle_event_gc_msg(self, account, array): 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) jids = array[0].split('/', 1)
room_jid = jids[0] room_jid = jids[0]
gc_control = self.msg_win_mgr.get_control(room_jid, account) gc_control = self.msg_win_mgr.get_control(room_jid, account)
@ -909,7 +918,7 @@ class Interface:
else: else:
# message from someone # message from someone
nick = jids[1] 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: if self.remote_ctrl:
self.remote_ctrl.raise_signal('GCMessage', (account, array)) self.remote_ctrl.raise_signal('GCMessage', (account, array))
@ -926,7 +935,8 @@ class Interface:
gc_control.print_conversation(array[2]) gc_control.print_conversation(array[2])
# ... Or the message comes from the occupant who set the subject # ... Or the message comes from the occupant who set the subject
elif len(jids) > 1: 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): def handle_event_gc_config(self, account, array):
#('GC_CONFIG', account, (jid, config)) config is a dict #('GC_CONFIG', account, (jid, config)) config is a dict

View file

@ -103,9 +103,10 @@ class PrivateChatControl(ChatControl):
if not message: if not message:
return return
# We need to make sure that we can still send through the room and that the # We need to make sure that we can still send through the room and that
# recipient did not go away # the recipient did not go away
contact = gajim.contacts.get_first_contact_from_jid(self.account, self.contact.jid) contact = gajim.contacts.get_first_contact_from_jid(self.account,
self.contact.jid)
if contact is None: if contact is None:
# contact was from pm in MUC # contact was from pm in MUC
room, nick = gajim.get_room_and_nick_from_fjid(self.contact.jid) 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] color = self.parent_win.notebook.style.fg[gtk.STATE_ACTIVE]
elif chatstate == 'newmsg' and (not has_focus or not current_tab) and\ elif chatstate == 'newmsg' and (not has_focus or not current_tab) and\
not self.attention_flag: 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: if color_name:
color = gtk.gdk.colormap_get_system().alloc_color(color_name) color = gtk.gdk.colormap_get_system().alloc_color(color_name)
@ -433,18 +435,18 @@ class GroupchatControl(ChatControlBase):
childs[3].set_sensitive(False) childs[3].set_sensitive(False)
return menu 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: if not nick:
# message from server # message from server
self.print_conversation(msg, tim = tim) self.print_conversation(msg, tim = tim, xhtml = xhtml)
else: else:
# message from someone # message from someone
if has_timestamp: if has_timestamp:
self.print_old_conversation(msg, nick, tim) self.print_old_conversation(msg, nick, tim, xhtml)
else: 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? # Do we have a queue?
fjid = self.room_jid + '/' + nick fjid = self.room_jid + '/' + nick
no_queue = len(gajim.events.get_events(self.account, fjid)) == 0 no_queue = len(gajim.events.get_events(self.account, fjid)) == 0
@ -452,7 +454,7 @@ class GroupchatControl(ChatControlBase):
# We print if window is opened # We print if window is opened
pm_control = gajim.interface.msg_win_mgr.get_control(fjid, self.account) pm_control = gajim.interface.msg_win_mgr.get_control(fjid, self.account)
if pm_control: if pm_control:
pm_control.print_conversation(msg, tim = tim) pm_control.print_conversation(msg, tim = tim, xhtml = xhtml)
return return
event = gajim.events.create_event('pm', (msg, '', 'incoming', tim, event = gajim.events.create_event('pm', (msg, '', 'incoming', tim,
@ -505,7 +507,7 @@ class GroupchatControl(ChatControlBase):
gc_count_nicknames_colors = 0 gc_count_nicknames_colors = 0
gc_custom_colors = {} 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): if isinstance(text, str):
text = unicode(text, 'utf-8') text = unicode(text, 'utf-8')
if contact == self.nick: # it's us if contact == self.nick: # it's us
@ -518,9 +520,9 @@ class GroupchatControl(ChatControlBase):
small_attr = [] small_attr = []
ChatControlBase.print_conversation_line(self, text, kind, contact, tim, ChatControlBase.print_conversation_line(self, text, kind, contact, tim,
small_attr, small_attr + ['restored_message'], 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: '''Print a line in the conversation:
if contact is set: it's a message from someone or an info message (contact if contact is set: it's a message from someone or an info message (contact
= 'info' in such a case) = 'info' in such a case)
@ -574,7 +576,7 @@ class GroupchatControl(ChatControlBase):
self.check_and_possibly_add_focus_out_line() self.check_and_possibly_add_focus_out_line()
ChatControlBase.print_conversation_line(self, text, kind, contact, tim, 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): def get_nb_unread(self):
nb = len(gajim.events.get_events(self.account, self.room_jid, nb = len(gajim.events.get_events(self.account, self.room_jid,
@ -643,13 +645,16 @@ class GroupchatControl(ChatControlBase):
word[char_position:char_position+1] word[char_position:char_position+1]
if (refer_to_nick_char != ''): if (refer_to_nick_char != ''):
refer_to_nick_char_code = ord(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)\ if ((refer_to_nick_char_code < 65 or \
or (refer_to_nick_char_code < 97 and refer_to_nick_char_code > 90)): refer_to_nick_char_code > 123) or \
(refer_to_nick_char_code < 97 and \
refer_to_nick_char_code > 90)):
return True return True
else: else:
# This is A->Z or a->z, we can be sure our nick is the beginning # This is A->Z or a->z, we can be sure our nick is the
# of a real word, do not highlight. Note that we can probably # beginning of a real word, do not highlight. Note that we
# do a better detection of non-punctuation characters # can probably do a better detection of non-punctuation
# characters
return False return False
else: # Special word == word, no char after in word else: # Special word == word, no char after in word
return True return True
@ -698,7 +703,8 @@ class GroupchatControl(ChatControlBase):
gc_contact.affiliation, gc_contact.status, gc_contact.affiliation, gc_contact.status,
gc_contact.jid) 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 '''opens a chat window and msg is not None sends private message to a
contact in a room''' contact in a room'''
if nick is None: if nick is None:
@ -707,14 +713,16 @@ class GroupchatControl(ChatControlBase):
self._start_private_message(nick) self._start_private_message(nick)
if msg: 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): def draw_contact(self, nick, selected=False, focus=False):
iter = self.get_contact_iter(nick) iter = self.get_contact_iter(nick)
if not iter: if not iter:
return return
model = self.list_treeview.get_model() 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'] state_images = gajim.interface.roster.jabber_state_images['16']
if len(gajim.events.get_events(self.account, self.room_jid + '/' + nick)): if len(gajim.events.get_events(self.account, self.room_jid + '/' + nick)):
image = state_images['message'] image = state_images['message']
@ -752,8 +760,8 @@ class GroupchatControl(ChatControlBase):
scaled_pixbuf = None scaled_pixbuf = None
model[iter][C_AVATAR] = scaled_pixbuf model[iter][C_AVATAR] = scaled_pixbuf
def chg_contact_status(self, nick, show, status, role, affiliation, jid, reason, actor, def chg_contact_status(self, nick, show, status, role, affiliation, jid,
statusCode, new_nick): reason, actor, statusCode, new_nick):
'''When an occupant changes his or her status''' '''When an occupant changes his or her status'''
if show == 'invisible': if show == 'invisible':
return return
@ -845,7 +853,8 @@ class GroupchatControl(ChatControlBase):
self.add_contact_to_roster(nick, show, role, self.add_contact_to_roster(nick, show, role,
affiliation, status, jid) affiliation, status, jid)
else: 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 \ if c.show == show and c.status == status and \
c.affiliation == affiliation: #no change c.affiliation == affiliation: #no change
return return
@ -948,7 +957,8 @@ class GroupchatControl(ChatControlBase):
iter = self.get_contact_iter(nick) iter = self.get_contact_iter(nick)
if not iter: if not iter:
return 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: if gc_contact:
gajim.contacts.remove_gc_contact(self.account, gc_contact) gajim.contacts.remove_gc_contact(self.account, gc_contact)
parent_iter = model.iter_parent(iter) parent_iter = model.iter_parent(iter)
@ -1096,7 +1106,8 @@ class GroupchatControl(ChatControlBase):
if len(message_array): if len(message_array):
message_array = message_array[0].split() message_array = message_array[0].split()
nick = message_array.pop(0) 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) reason = ' '.join(message_array)
if nick in room_nicks: if nick in room_nicks:
ban_jid = gajim.construct_fjid(self.room_jid, nick) ban_jid = gajim.construct_fjid(self.room_jid, nick)
@ -1117,7 +1128,8 @@ class GroupchatControl(ChatControlBase):
if len(message_array): if len(message_array):
message_array = message_array[0].split() message_array = message_array[0].split()
nick = message_array.pop(0) 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) reason = ' '.join(message_array)
if nick in room_nicks: if nick in room_nicks:
gajim.connections[self.account].gc_set_role(self.room_jid, nick, gajim.connections[self.account].gc_set_role(self.room_jid, nick,
@ -1177,7 +1189,8 @@ class GroupchatControl(ChatControlBase):
if not self._process_command(message): if not self._process_command(message):
# Send the 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.get_buffer().set_text('')
self.msg_textview.grab_focus() self.msg_textview.grab_focus()
@ -1200,7 +1213,8 @@ class GroupchatControl(ChatControlBase):
self.print_conversation(_('Usage: /%s [reason], closes the current ' self.print_conversation(_('Usage: /%s [reason], closes the current '
'window or tab, displaying reason if specified.') % command, 'info') 'window or tab, displaying reason if specified.') % command, 'info')
elif command == 'compact': 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': elif command == 'invite':
self.print_conversation(_('Usage: /%s <JID> [reason], invites JID to ' self.print_conversation(_('Usage: /%s <JID> [reason], invites JID to '
'the current room, optionally providing a reason.') % command, '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') self.print_conversation(_('No help info for /%s') % command, 'info')
def get_role(self, nick): 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: if gc_contact:
return gc_contact.role return gc_contact.role
else: else:
@ -1327,7 +1342,8 @@ class GroupchatControl(ChatControlBase):
_('Please specify the new subject:'), self.subject) _('Please specify the new subject:'), self.subject)
response = instance.get_response() response = instance.get_response()
if response == gtk.RESPONSE_OK: 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') subject = instance.input_entry.get_text().decode('utf-8')
gajim.connections[self.account].send_gc_subject(self.room_jid, subject) gajim.connections[self.account].send_gc_subject(self.room_jid, subject)
@ -1372,7 +1388,8 @@ class GroupchatControl(ChatControlBase):
_('Bookmark has been added successfully'), _('Bookmark has been added successfully'),
_('You can manage your bookmarks via Actions menu in your roster.')) _('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 # NOTE: handles mykeypress which is custom signal connected to this
# CB in new_room(). for this singal see message_textview.py # CB in new_room(). for this singal see message_textview.py
@ -1384,12 +1401,14 @@ class GroupchatControl(ChatControlBase):
message_buffer = widget.get_buffer() message_buffer = widget.get_buffer()
start_iter, end_iter = message_buffer.get_bounds() 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 if event.keyval == gtk.keysyms.Tab: # TAB
cursor_position = message_buffer.get_insert() cursor_position = message_buffer.get_insert()
end_iter = message_buffer.get_iter_at_mark(cursor_position) 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 text.endswith(' '):
if not self.last_key_tabs: if not self.last_key_tabs:
return False return False
@ -1505,8 +1524,8 @@ class GroupchatControl(ChatControlBase):
# looking for user's affiliation and role # looking for user's affiliation and role
user_nick = self.nick user_nick = self.nick
user_affiliation = gajim.contacts.get_gc_contact(self.account, self.room_jid, user_affiliation = gajim.contacts.get_gc_contact(self.account,
user_nick).affiliation self.room_jid, user_nick).affiliation
user_role = self.get_role(user_nick) user_role = self.get_role(user_nick)
# making menu from glade # making menu from glade
@ -1516,8 +1535,9 @@ class GroupchatControl(ChatControlBase):
item = xml.get_widget('kick_menuitem') item = xml.get_widget('kick_menuitem')
if user_role != 'moderator' or \ if user_role != 'moderator' or \
(user_affiliation == 'admin' and target_affiliation == 'owner') or \ (user_affiliation == 'admin' and target_affiliation == 'owner') or \
(user_affiliation == 'member' and target_affiliation in ('admin', 'owner')) or \ (user_affiliation == 'member' and target_affiliation in ('admin',
(user_affiliation == 'none' and target_affiliation != 'none'): 'owner')) or (user_affiliation == 'none' and target_affiliation != \
'none'):
item.set_sensitive(False) item.set_sensitive(False)
id = item.connect('activate', self.kick, nick) id = item.connect('activate', self.kick, nick)
self.handlers[id] = item self.handlers[id] = item
@ -1743,19 +1763,23 @@ class GroupchatControl(ChatControlBase):
def grant_voice(self, widget, nick): def grant_voice(self, widget, nick):
'''grant voice privilege to a user''' '''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): def revoke_voice(self, widget, nick):
'''revoke voice privilege to a user''' '''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): def grant_moderator(self, widget, nick):
'''grant moderator privilege to a user''' '''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): def revoke_moderator(self, widget, nick):
'''revoke moderator privilege to a user''' '''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): def ban(self, widget, jid):
'''ban a user''' '''ban a user'''
@ -1804,7 +1828,8 @@ class GroupchatControl(ChatControlBase):
c = gajim.contacts.get_gc_contact(self.account, self.room_jid, nick) c = gajim.contacts.get_gc_contact(self.account, self.room_jid, nick)
c2 = gajim.contacts.contact_from_gc_contact(c) c2 = gajim.contacts.contact_from_gc_contact(c)
if gajim.interface.instances[self.account]['infos'].has_key(c2.jid): 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: else:
gajim.interface.instances[self.account]['infos'][c2.jid] = \ gajim.interface.instances[self.account]['infos'][c2.jid] = \
vcard.VcardWindow(c2, self.account, is_fake = True) vcard.VcardWindow(c2, self.account, is_fake = True)

968
src/htmltextview.py Normal file
View file

@ -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 <b>, <i> or <u> are not in the JEP-0071, but are seen in the wild
basic_pattern = r'(?<!\w|\<|/|:)' r'/[^\s/]' r'([^/]*[^\s/])?' r'/(?!\w|/|:)|'\
r'(?<!\w)' r'_[^\s_]' r'([^_]*[^\s_])?' r'_(?!\w)'
view.basic_pattern_re = re.compile(basic_pattern)
#TODO: emoticons
emoticons_pattern = ''
if config.get('emoticons_theme'):
# When an emoticon is bordered by an alpha-numeric character it is NOT
# expanded. e.g., foo:) NO, foo :) YES, (brb) NO, (:)) YES, etc.
# We still allow multiple emoticons side-by-side like :P:P:P
# sort keys by length so :qwe emot is checked before :q
keys = interface.emoticons.keys()
keys.sort(interface.on_emoticon_sort)
emoticons_pattern_prematch = ''
emoticons_pattern_postmatch = ''
emoticon_length = 0
for emoticon in keys: # travel thru emoticons list
emoticon_escaped = re.escape(emoticon) # espace regexp metachars
emoticons_pattern += emoticon_escaped + '|'# | means or in regexp
if (emoticon_length != len(emoticon)):
# Build up expressions to match emoticons next to other emoticons
emoticons_pattern_prematch = emoticons_pattern_prematch[:-1] + ')|(?<='
emoticons_pattern_postmatch = emoticons_pattern_postmatch[:-1] + ')|(?='
emoticon_length = len(emoticon)
emoticons_pattern_prematch += emoticon_escaped + '|'
emoticons_pattern_postmatch += emoticon_escaped + '|'
# We match from our list of emoticons, but they must either have
# whitespace, or another emoticon next to it to match successfully
# [\w.] alphanumeric and dot (for not matching 8) in (2.8))
emoticons_pattern = '|' + \
'(?:(?<![\w.]' + emoticons_pattern_prematch[:-1] + '))' + \
'(?:' + emoticons_pattern[:-1] + ')' + \
'(?:(?![\w.]' + emoticons_pattern_postmatch[:-1] + '))'
# because emoticons match later (in the string) they need to be after
# basic matches that may occur earlier
emot_and_basic_pattern = basic_pattern + emoticons_pattern
view.emot_and_basic_re = re.compile(emot_and_basic_pattern, re.IGNORECASE)
def _parse_css_color(color):
'''_parse_css_color(css_color) -> 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('<div><span style="color: red; text-decoration:underline">Hello</span><br/>\n'
' <img src="http://images.slashdot.org/topics/topicsoftware.gif"/><br/>\n'
' <span style="font-size: 500%; font-family: serif">World</span>\n'
'</div>\n')
htmlview.display_html("<hr />")
htmlview.display_html("""
<p style='font-size:large'>
<span style='font-style: italic'>O<span style='font-size:larger'>M</span>G</span>,
I&apos;m <span style='color:green'>green</span>
with <span style='font-weight: bold'>envy</span>!
</p>
""")
htmlview.display_html("<hr />")
htmlview.display_html("""
<body xmlns='http://www.w3.org/1999/xhtml'>
<p>As Emerson said in his essay <span style='font-style: italic; background-color:cyan'>Self-Reliance</span>:</p>
<p style='margin-left: 5px; margin-right: 2%'>
&quot;A foolish consistency is the hobgoblin of little minds.&quot;
</p>
</body>
""")
htmlview.display_html("<hr />")
htmlview.display_html("""
<body xmlns='http://www.w3.org/1999/xhtml'>
<p style='text-align:center'>Hey, are you licensed to <a href='http://www.jabber.org/'>Jabber</a>?</p>
<p style='text-align:right'><img src='http://www.jabber.org/images/psa-license.jpg'
alt='A License to Jabber'
height='261'
width='537'/></p>
</body>
""")
htmlview.display_html("<hr />")
htmlview.display_html("""
<body xmlns='http://www.w3.org/1999/xhtml'>
<ul style='background-color:rgb(120,140,100)'>
<li> One </li>
<li> Two </li>
<li> Three </li>
</ul><hr /><pre style="background-color:rgb(120,120,120)">def fac(n):
def faciter(n,acc):
if n==0: return acc
return faciter(n-1, acc*n)
if n&lt;0: raise ValueError("Must be non-negative")
return faciter(n,1)</pre>
</body>
""")
htmlview.display_html("<hr />")
htmlview.display_html("""
<body xmlns='http://www.w3.org/1999/xhtml'>
<ol style='background-color:rgb(120,140,100)'>
<li> One </li>
<li> Two is nested: <ul style='background-color:rgb(200,200,100)'>
<li> One </li>
<li> Two </li>
<li> Three </li>
</ul></li>
<li> Three </li></ol>
</body>
""")
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()

View file

@ -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, def on_message(self, jid, msg, tim, account, encrypted = False,
msg_type = '', subject = None, resource = '', msg_id = None, 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''' '''when we receive a message'''
contact = None contact = None
# if chat window will be for specific resource # 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': if msg_type == 'error':
typ = 'status' typ = 'status'
ctrl.print_conversation(msg, typ, tim = tim, encrypted = encrypted, ctrl.print_conversation(msg, typ, tim = tim, encrypted = encrypted,
subject = subject) subject = subject, xhtml = xhtml)
if msg_id: if msg_id:
gajim.logger.set_read_messages([msg_id]) gajim.logger.set_read_messages([msg_id])
return 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_roster = notify.get_show_in_roster(event_type, account, contact)
show_in_systray = notify.get_show_in_systray(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, 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) show_in_systray = show_in_systray)
gajim.events.add_event(account, fjid, event) gajim.events.add_event(account, fjid, event)
if popup: if popup: