[Santiago Gala] we can now see XHTML (JEP 0071). See #316
This commit is contained in:
parent
aeb6116ba6
commit
6b40b5ad32
9 changed files with 1202 additions and 132 deletions
|
@ -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()
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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')):
|
||||||
|
|
34
src/gajim.py
34
src/gajim.py
|
@ -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
|
||||||
|
|
|
@ -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
968
src/htmltextview.py
Normal 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'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,
|
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:
|
||||||
|
|
Loading…
Add table
Reference in a new issue