[Santiago Gala] we can now see XHTML (JEP 0071). See #316
This commit is contained in:
parent
aeb6116ba6
commit
6b40b5ad32
|
@ -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()
|
||||
|
|
|
@ -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 <body>, add <id>
|
||||
if not msg_id: # avoid putting 'None' in <id> 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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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('<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):
|
||||
""" Sets the subject of the message. """
|
||||
self.setTagData('subject',val)
|
||||
|
|
|
@ -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', '<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()
|
||||
# /me is replaced by name if name is given
|
||||
if name and (text.startswith('/me ') or text.startswith('/me\n')):
|
||||
|
|
38
src/gajim.py
38
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)
|
||||
if encrypted:
|
||||
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)
|
||||
# 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
|
||||
|
|
|
@ -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 <JID> [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
|
||||
|
@ -1516,8 +1535,9 @@ class GroupchatControl(ChatControlBase):
|
|||
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 == '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
|
||||
|
@ -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'''
|
||||
|
@ -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)
|
||||
|
|
|
@ -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'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%'>
|
||||
"A foolish consistency is the hobgoblin of little minds."
|
||||
</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<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()
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue