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

+ OMG, + I'm green + with envy! +

+ """) + htmlview.display_html("
") + htmlview.display_html(""" + +

As Emerson said in his essay Self-Reliance:

+

+ "A foolish consistency is the hobgoblin of little minds." +

+ + """) + htmlview.display_html("
") + htmlview.display_html(""" + +

Hey, are you licensed to Jabber?

+

A License to Jabber

+ + """) + htmlview.display_html("
") + htmlview.display_html(""" + +
    +
  • One
  • +
  • Two
  • +
  • Three
  • +

def fac(n):
+  def faciter(n,acc): 
+	if n==0: return acc
+	return faciter(n-1, acc*n)
+  if n<0: raise ValueError("Must be non-negative")
+  return faciter(n,1)
+ + """) + htmlview.display_html("
") + htmlview.display_html(""" + +
    +
  1. One
  2. +
  3. Two is nested:
      +
    • One
    • +
    • Two
    • +
    • Three
    • +
  4. +
  5. Three
+ + """) + htmlview.show() + sw = gtk.ScrolledWindow() + sw.set_property("hscrollbar-policy", gtk.POLICY_AUTOMATIC) + sw.set_property("vscrollbar-policy", gtk.POLICY_AUTOMATIC) + sw.set_property("border-width", 0) + sw.add(htmlview) + sw.show() + frame = gtk.Frame() + frame.set_shadow_type(gtk.SHADOW_IN) + frame.show() + frame.add(sw) + w = gtk.Window() + w.add(frame) + w.set_default_size(400, 300) + w.show_all() + w.connect("destroy", lambda w: gtk.main_quit()) + gtk.main() diff --git a/src/roster_window.py b/src/roster_window.py index b2268e921..2f5f08083 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -2531,7 +2531,7 @@ _('If "%s" accepts this request you will know his or her status.') % jid) def on_message(self, jid, msg, tim, account, encrypted = False, msg_type = '', subject = None, resource = '', msg_id = None, - user_nick = '', advanced_notif_num = None): + user_nick = '', advanced_notif_num = None, xhtml = None): '''when we receive a message''' contact = None # if chat window will be for specific resource @@ -2599,7 +2599,7 @@ _('If "%s" accepts this request you will know his or her status.') % jid) if msg_type == 'error': typ = 'status' ctrl.print_conversation(msg, typ, tim = tim, encrypted = encrypted, - subject = subject) + subject = subject, xhtml = xhtml) if msg_id: gajim.logger.set_read_messages([msg_id]) return @@ -2613,7 +2613,7 @@ _('If "%s" accepts this request you will know his or her status.') % jid) show_in_roster = notify.get_show_in_roster(event_type, account, contact) show_in_systray = notify.get_show_in_systray(event_type, account, contact) event = gajim.events.create_event(type_, (msg, subject, msg_type, tim, - encrypted, resource, msg_id), show_in_roster = show_in_roster, + encrypted, resource, msg_id, xhtml), show_in_roster = show_in_roster, show_in_systray = show_in_systray) gajim.events.add_event(account, fjid, event) if popup: