root/branches/gajim_0.10.1/src/conversation_textview.py

Revision 6407, 24.5 kB (checked in by dkirov, 2 years ago)

r6265, r6266, r6267, r6269, r6350, r6366

Line 
1##      conversation_textview.py
2##
3## Contributors for this file:
4##      - Yann Le Boulanger <asterix@lagaule.org>
5##      - Nikos Kouremenos <kourem@gmail.com>
6##
7## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org>
8##                         Vincent Hanquez <tab@snarc.org>
9## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
10##                    Vincent Hanquez <tab@snarc.org>
11##                    Nikos Kouremenos <nkour@jabber.org>
12##                    Dimitur Kirov <dkirov@gmail.com>
13##                    Travis Shirk <travis@pobox.com>
14##                    Norman Rasmussen <norman@rasmussen.co.za>
15##
16## This program is free software; you can redistribute it and/or modify
17## it under the terms of the GNU General Public License as published
18## by the Free Software Foundation; version 2 only.
19##
20## This program is distributed in the hope that it will be useful,
21## but WITHOUT ANY WARRANTY; without even the implied warranty of
22## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23## GNU General Public License for more details.
24##
25
26import gtk
27import gtk.glade
28import pango
29import gobject
30import time
31import sys
32import tooltips
33import dialogs
34import locale
35
36import gtkgui_helpers
37from common import gajim
38from common import helpers
39from common import i18n
40from calendar import timegm
41
42_ = i18n._
43APP = i18n.APP
44gtk.glade.bindtextdomain(APP, i18n.DIR)
45gtk.glade.textdomain(APP)
46
47class ConversationTextview:
48        '''Class for the conversation textview (where user reads already said messages)
49        for chat/groupchat windows'''
50        def __init__(self, account):
51                # no need to inherit TextView, use it as property is safer
52                self.tv = gtk.TextView()
53
54                # set properties
55                self.tv.set_border_width(1)
56                self.tv.set_accepts_tab(True)
57                self.tv.set_editable(False)
58                self.tv.set_cursor_visible(False)
59                self.tv.set_wrap_mode(gtk.WRAP_WORD)
60                self.tv.set_left_margin(2)
61                self.tv.set_right_margin(2)
62                self.handlers = {}
63
64                # connect signals
65                id = self.tv.connect('motion_notify_event', self.on_textview_motion_notify_event)
66                self.handlers[id] = self.tv
67                id = self.tv.connect('populate_popup', self.on_textview_populate_popup)
68                self.handlers[id] = self.tv
69                id = self.tv.connect('button_press_event', self.on_textview_button_press_event)
70                self.handlers[id] = self.tv
71
72                self.account = account
73                self.change_cursor = None
74                self.last_time_printout = 0
75
76                font = pango.FontDescription(gajim.config.get('conversation_font'))
77                self.tv.modify_font(font)
78                buffer = self.tv.get_buffer()
79                end_iter = buffer.get_end_iter()
80                buffer.create_mark('end', end_iter, False)
81
82                self.tagIn = buffer.create_tag('incoming')
83                color = gajim.config.get('inmsgcolor')
84                self.tagIn.set_property('foreground', color)
85                self.tagOut = buffer.create_tag('outgoing')
86                color = gajim.config.get('outmsgcolor')
87                self.tagOut.set_property('foreground', color)
88                self.tagStatus = buffer.create_tag('status')
89                color = gajim.config.get('statusmsgcolor')
90                self.tagStatus.set_property('foreground', color)
91
92                tag = buffer.create_tag('marked')
93                color = gajim.config.get('markedmsgcolor')
94                tag.set_property('foreground', color)
95                tag.set_property('weight', pango.WEIGHT_BOLD)
96
97                tag = buffer.create_tag('time_sometimes')
98                tag.set_property('foreground', 'grey')
99                tag.set_property('scale', pango.SCALE_SMALL)
100                tag.set_property('justification', gtk.JUSTIFY_CENTER)
101
102                tag = buffer.create_tag('small')
103                tag.set_property('scale', pango.SCALE_SMALL)
104
105                tag = buffer.create_tag('restored_message')
106                color = gajim.config.get('restored_messages_color')
107                tag.set_property('foreground', color)
108
109                tag = buffer.create_tag('url')
110                color = gajim.config.get('urlmsgcolor')
111                tag.set_property('foreground', color)
112                tag.set_property('underline', pango.UNDERLINE_SINGLE)
113                id = tag.connect('event', self.hyperlink_handler, 'url')
114                self.handlers[id] = tag
115
116                tag = buffer.create_tag('mail')
117                tag.set_property('foreground', color)
118                tag.set_property('underline', pango.UNDERLINE_SINGLE)
119                id = tag.connect('event', self.hyperlink_handler, 'mail')
120                self.handlers[id] = tag
121
122                tag = buffer.create_tag('bold')
123                tag.set_property('weight', pango.WEIGHT_BOLD)
124
125                tag = buffer.create_tag('italic')
126                tag.set_property('style', pango.STYLE_ITALIC)
127
128                tag = buffer.create_tag('underline')
129                tag.set_property('underline', pango.UNDERLINE_SINGLE)
130
131                buffer.create_tag('focus-out-line', justification = gtk.JUSTIFY_CENTER)
132
133                self.line_tooltip = tooltips.BaseTooltip()
134
135        def del_handlers(self):
136                for i in self.handlers.keys():
137                        if self.handlers[i].handler_is_connected(i):
138                                self.handlers[i].disconnect(i)
139                del self.handlers
140                self.tv.destroy()
141                #TODO
142                # self.line_tooltip.destroy()
143       
144        def update_tags(self):
145                self.tagIn.set_property('foreground', gajim.config.get('inmsgcolor'))
146                self.tagOut.set_property('foreground', gajim.config.get('outmsgcolor'))
147                self.tagStatus.set_property('foreground',
148                        gajim.config.get('statusmsgcolor'))
149
150        def at_the_end(self):
151                buffer = self.tv.get_buffer()
152                end_iter = buffer.get_end_iter()
153                end_rect = self.tv.get_iter_location(end_iter)
154                visible_rect = self.tv.get_visible_rect()
155                if end_rect.y <= (visible_rect.y + visible_rect.height):
156                        return True
157                return False
158
159        def scroll_to_end(self):
160                parent = self.tv.get_parent()
161                buffer = self.tv.get_buffer()
162                end_mark = buffer.get_mark('end')
163                if not end_mark:
164                        return False
165                self.tv.scroll_to_mark(end_mark, 0, True, 0, 1)
166                adjustment = parent.get_hadjustment()
167                adjustment.set_value(0)
168                return False # when called in an idle_add, just do it once
169
170        def bring_scroll_to_end(self, diff_y = 0):
171                ''' scrolls to the end of textview if end is not visible '''
172                buffer = self.tv.get_buffer()
173                end_iter = buffer.get_end_iter()
174                end_rect = self.tv.get_iter_location(end_iter)
175                visible_rect = self.tv.get_visible_rect()
176                # scroll only if expected end is not visible
177                if end_rect.y >= (visible_rect.y + visible_rect.height + diff_y):
178                        gobject.idle_add(self.scroll_to_end_iter)
179
180        def scroll_to_end_iter(self):
181                buffer = self.tv.get_buffer()
182                end_iter = buffer.get_end_iter()
183                self.tv.scroll_to_iter(end_iter, 0, False, 1, 1)
184                return False # when called in an idle_add, just do it once
185
186        def show_line_tooltip(self):
187                pointer = self.tv.get_pointer()
188                x, y = self.tv.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, pointer[0],
189                        pointer[1])
190                tags = self.tv.get_iter_at_location(x, y).get_tags()
191                tag_table = self.tv.get_buffer().get_tag_table()
192                over_line = False
193                for tag in tags:
194                        if tag == tag_table.lookup('focus-out-line'):
195                                over_line = True
196                                break
197                if over_line and not self.line_tooltip.win:
198                        # check if the current pointer is still over the line
199                        position = self.tv.window.get_origin()
200                        win = self.tv.get_toplevel()
201                        self.line_tooltip.show_tooltip(_('Text below this line is what has '
202                        'been said since the last time you paid attention to this group chat'), 8, position[1] + pointer[1])
203
204        def on_textview_motion_notify_event(self, widget, event):
205                '''change the cursor to a hand when we are over a mail or an url'''
206                pointer_x, pointer_y, spam = self.tv.window.get_pointer()
207                x, y = self.tv.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, pointer_x,
208                        pointer_y)
209                tags = self.tv.get_iter_at_location(x, y).get_tags()
210                if self.change_cursor:
211                        self.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
212                                gtk.gdk.Cursor(gtk.gdk.XTERM))
213                        self.change_cursor = None
214                tag_table = self.tv.get_buffer().get_tag_table()
215                over_line = False
216                for tag in tags:
217                        if tag in (tag_table.lookup('url'), tag_table.lookup('mail')):
218                                self.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
219                                        gtk.gdk.Cursor(gtk.gdk.HAND2))
220                                self.change_cursor = tag
221                        elif tag == tag_table.lookup('focus-out-line'):
222                                over_line = True
223
224                if self.line_tooltip.timeout != 0:
225                        # Check if we should hide the line tooltip
226                        if not over_line:
227                                self.line_tooltip.hide_tooltip()
228                if over_line and not self.line_tooltip.win:
229                        self.line_tooltip.timeout = gobject.timeout_add(500,
230                                self.show_line_tooltip)
231                        self.tv.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
232                                gtk.gdk.Cursor(gtk.gdk.LEFT_PTR))
233                        self.change_cursor = tag
234
235        def clear(self, tv = None):
236                '''clear text in the textview'''
237                buffer = self.tv.get_buffer()
238                start, end = buffer.get_bounds()
239                buffer.delete(start, end)
240
241        def visit_url_from_menuitem(self, widget, link):
242                '''basically it filters out the widget instance'''
243                helpers.launch_browser_mailer('url', link)
244
245        def on_textview_populate_popup(self, textview, menu):
246                '''we override the default context menu and we prepend Clear
247                and if we have sth selected we show a submenu with actions on the phrase
248                (see on_conversation_textview_button_press_event)'''
249                item = gtk.SeparatorMenuItem()
250                menu.prepend(item)
251                item = gtk.ImageMenuItem(gtk.STOCK_CLEAR)
252                menu.prepend(item)
253                id = item.connect('activate', self.clear)
254                self.handlers[id] = item
255                if self.selected_phrase:
256                        s = self.selected_phrase
257                        if len(s) > 25:
258                                s = s[:21] + '...'
259                        item = gtk.MenuItem(_('Actions for "%s"') % s)
260                        menu.prepend(item)
261                        submenu = gtk.Menu()
262                        item.set_submenu(submenu)
263
264                        always_use_en = gajim.config.get('always_english_wikipedia')
265                        if always_use_en:
266                                link = 'http://en.wikipedia.org/wiki/Special:Search?search=%s'\
267                                        % self.selected_phrase
268                        else:
269                                link = 'http://%s.wikipedia.org/wiki/Special:Search?search=%s'\
270                                        % (gajim.LANG, self.selected_phrase)
271                        item = gtk.MenuItem(_('Read _Wikipedia Article'))
272                        id = item.connect('activate', self.visit_url_from_menuitem, link)
273                        self.handlers[id] = item
274                        submenu.append(item)
275
276                        item = gtk.MenuItem(_('Look it up in _Dictionary'))
277                        dict_link = gajim.config.get('dictionary_url')
278                        if dict_link == 'WIKTIONARY':
279                                # special link (yeah undocumented but default)
280                                always_use_en = gajim.config.get('always_english_wiktionary')
281                                if always_use_en:
282                                        link = 'http://en.wiktionary.org/wiki/Special:Search?search=%s'\
283                                                % self.selected_phrase
284                                else:
285                                        link = 'http://%s.wiktionary.org/wiki/Special:Search?search=%s'\
286                                                % (gajim.LANG, self.selected_phrase)
287                                id = item.connect('activate', self.visit_url_from_menuitem, link)
288                                self.handlers[id] = item
289                        else:
290                                if dict_link.find('%s') == -1:
291                                        #we must have %s in the url if not WIKTIONARY
292                                        item = gtk.MenuItem(_('Dictionary URL is missing an "%s" and it is not WIKTIONARY'))
293                                        item.set_property('sensitive', False)
294                                else:
295                                        link = dict_link % self.selected_phrase
296                                        id = item.connect('activate', self.visit_url_from_menuitem, link)
297                                        self.handlers[id] = item
298                        submenu.append(item)
299
300
301                        search_link = gajim.config.get('search_engine')
302                        if search_link.find('%s') == -1:
303                                #we must have %s in the url
304                                item = gtk.MenuItem(_('Web Search URL is missing an "%s"'))
305                                item.set_property('sensitive', False)
306                        else:
307                                item = gtk.MenuItem(_('Web _Search for it'))
308                                link =  search_link % self.selected_phrase
309                                id = item.connect('activate', self.visit_url_from_menuitem, link)
310                                self.handlers[id] = item
311                        submenu.append(item)
312
313                menu.show_all()
314
315        def on_textview_button_press_event(self, widget, event):
316                # If we clicked on a taged text do NOT open the standard popup menu
317                # if normal text check if we have sth selected
318
319                self.selected_phrase = ''
320
321                if event.button != 3: # if not right click
322                        return False
323
324                win = self.tv.get_window(gtk.TEXT_WINDOW_TEXT)
325                x, y = self.tv.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT,
326                        int(event.x), int(event.y))
327                iter = self.tv.get_iter_at_location(x, y)
328                tags = iter.get_tags()
329
330
331                if tags: # we clicked on sth special (it can be status message too)
332                        for tag in tags:
333                                tag_name = tag.get_property('name')
334                                if 'url' in tag_name or 'mail' in tag_name:
335                                        return True # we block normal context menu
336
337                # we check if sth was selected and if it was we assign
338                # selected_phrase variable
339                # so on_conversation_textview_populate_popup can use it
340                buffer = self.tv.get_buffer()
341                return_val = buffer.get_selection_bounds()
342                if return_val: # if sth was selected when we right-clicked
343                        # get the selected text
344                        start_sel, finish_sel = return_val[0], return_val[1]
345                        self.selected_phrase = buffer.get_text(start_sel, finish_sel).decode('utf-8')
346
347        def on_open_link_activate(self, widget, kind, text):
348                helpers.launch_browser_mailer(kind, text)
349
350        def on_copy_link_activate(self, widget, text):
351                clip = gtk.clipboard_get()
352                clip.set_text(text)
353
354        def on_start_chat_activate(self, widget, jid):
355                gajim.interface.roster.new_chat_from_jid(self.account, jid)
356
357        def on_join_group_chat_menuitem_activate(self, widget, jid):
358                room, server = jid.split('@')
359                if gajim.interface.instances[self.account].has_key('join_gc'):
360                        instance = gajim.interface.instances[self.account]['join_gc']
361                        instance.xml.get_widget('server_entry').set_text(server)
362                        instance.xml.get_widget('room_entry').set_text(room)
363                        gajim.interface.instances[self.account]['join_gc'].window.present()
364                else:
365                        try:
366                                gajim.interface.instances[self.account]['join_gc'] = \
367                                dialogs.JoinGroupchatWindow(self.account, server, room)
368                        except RuntimeError:
369                                pass
370
371        def on_add_to_roster_activate(self, widget, jid):
372                dialogs.AddNewContactWindow(self.account, jid)
373
374        def make_link_menu(self, event, kind, text):
375                xml = gtkgui_helpers.get_glade('chat_context_menu.glade')
376                menu = xml.get_widget('chat_context_menu')
377                childs = menu.get_children()
378                if kind == 'url':
379                        id = childs[0].connect('activate', self.on_copy_link_activate, text)
380                        self.handlers[id] = childs[0]
381                        id = childs[1].connect('activate', self.on_open_link_activate, kind, text)
382                        self.handlers[id] = childs[1]
383                        childs[2].hide() # copy mail address
384                        childs[3].hide() # open mail composer
385                        childs[4].hide() # jid section separator
386                        childs[5].hide() # start chat
387                        childs[6].hide() # join group chat
388                        childs[7].hide() # add to roster
389                else: # It's a mail or a JID
390                        id = childs[2].connect('activate', self.on_copy_link_activate, text)
391                        self.handlers[id] = childs[2]
392                        id = childs[3].connect('activate', self.on_open_link_activate, kind, text)
393                        self.handlers[id] = childs[3]
394                        id = childs[5].connect('activate', self.on_start_chat_activate, text)
395                        self.handlers[id] = childs[5]
396                        id = childs[6].connect('activate',
397                                self.on_join_group_chat_menuitem_activate, text)
398                        self.handlers[id] = childs[6]
399
400                        allow_add = False
401                        c = gajim.contacts.get_first_contact_from_jid(self.account, text)
402                        if c and not gajim.contacts.is_pm_from_contact(self.account, c):
403                                if _('Not in Roster') in c.groups:
404                                        allow_add = True
405                        else: # he or she's not at all in the account contacts
406                                allow_add = True
407
408                        if allow_add:
409                                id = childs[7].connect('activate', self.on_add_to_roster_activate, text)
410                                self.handlers[id] = childs[7]
411                                childs[7].show() # show add to roster menuitem
412                        else:
413                                childs[7].hide() # hide add to roster menuitem
414
415                        childs[0].hide() # copy link location
416                        childs[1].hide() # open link in browser
417
418                menu.popup(None, None, None, event.button, event.time)
419
420        def hyperlink_handler(self, texttag, widget, event, iter, kind):
421                if event.type == gtk.gdk.BUTTON_PRESS:
422                        begin_iter = iter.copy()
423                        # we get the begining of the tag
424                        while not begin_iter.begins_tag(texttag):
425                                begin_iter.backward_char()
426                        end_iter = iter.copy()
427                        # we get the end of the tag
428                        while not end_iter.ends_tag(texttag):
429                                end_iter.forward_char()
430                        word = self.tv.get_buffer().get_text(begin_iter, end_iter).decode('utf-8')
431                        if event.button == 3: # right click
432                                self.make_link_menu(event, kind, word)
433                        else:
434                                # we launch the correct application
435                                helpers.launch_browser_mailer(kind, word)
436
437        def detect_and_print_special_text(self, otext, other_tags):
438                '''detects special text (emots & links & formatting)
439                prints normal text before any special text it founts,
440                then print special text (that happens many times until
441                last special text is printed) and then returns the index
442                after *last* special text, so we can print it in
443                print_conversation_line()'''
444
445                buffer = self.tv.get_buffer()
446
447                start = 0
448                end = 0
449                index = 0
450
451                # basic: links + mail + formatting is always checked (we like that)
452                if gajim.config.get('emoticons_theme'): # search for emoticons & urls
453                        iterator = gajim.interface.emot_and_basic_re.finditer(otext)
454                else: # search for just urls + mail + formatting
455                        iterator = gajim.interface.basic_pattern_re.finditer(otext)
456                for match in iterator:
457                        start, end = match.span()
458                        special_text = otext[start:end]
459                        if start != 0:
460                                text_before_special_text = otext[index:start]
461                                end_iter = buffer.get_end_iter()
462                                # we insert normal text
463                                buffer.insert_with_tags_by_name(end_iter,
464                                        text_before_special_text, *other_tags)
465                        index = end # update index
466
467                        # now print it
468                        self.print_special_text(special_text, other_tags)
469
470                return index # the position after *last* special text
471
472        def print_special_text(self, special_text, other_tags):
473                '''is called by detect_and_print_special_text and prints
474                special text (emots, links, formatting)'''
475                tags = []
476                use_other_tags = True
477                show_ascii_formatting_chars = \
478                        gajim.config.get('show_ascii_formatting_chars')
479                buffer = self.tv.get_buffer()
480
481                possible_emot_ascii_caps = special_text.upper() # emoticons keys are CAPS
482                if gajim.config.get('emoticons_theme') and \
483                possible_emot_ascii_caps in gajim.interface.emoticons.keys():
484                        #it's an emoticon
485                        emot_ascii = possible_emot_ascii_caps
486                        end_iter = buffer.get_end_iter()
487                        anchor = buffer.create_child_anchor(end_iter)
488                        img = gtk.Image()
489                        img.set_from_file(gajim.interface.emoticons[emot_ascii])
490                        img.show()
491                        #add with possible animation
492                        self.tv.add_child_at_anchor(img, anchor)
493                elif special_text.startswith('http://') or \
494                        special_text.startswith('www.') or \
495                        special_text.startswith('ftp://') or \
496                        special_text.startswith('ftp.') or \
497                        special_text.startswith('https://') or \
498                        special_text.startswith('gopher://') or \
499                        special_text.startswith('news://') or \
500                        special_text.startswith('ed2k://') or \
501                        special_text.startswith('irc://') or \
502                        special_text.startswith('sip:') or \
503                        special_text.startswith('magnet:'):
504                        #it's a url
505                        tags.append('url')
506                        use_other_tags = False
507                elif special_text.startswith('mailto:'):
508                        #it's a mail
509                        tags.append('mail')
510                        use_other_tags = False
511                elif gajim.interface.sth_at_sth_dot_sth_re.match(special_text):
512                        #it's a mail
513                        tags.append('mail')
514                        use_other_tags = False
515                elif special_text.startswith('*'): # it's a bold text
516                        tags.append('bold')
517                        if special_text[1] == '/' and special_text[-2] == '/' and len(special_text) > 4: # it's also italic
518                                tags.append('italic')
519                                if not show_ascii_formatting_chars:
520                                        special_text = special_text[2:-2] # remove */ /*
521                        elif special_text[1] == '_' and special_text[-2] == '_' and len(special_text) > 4: # it's also underlined
522                                tags.append('underline')
523                                if not show_ascii_formatting_chars:
524                                        special_text = special_text[2:-2] # remove *_ _*
525                        else:
526                                if not show_ascii_formatting_chars:
527                                        special_text = special_text[1:-1] # remove * *
528                elif special_text.startswith('/'): # it's an italic text
529                        tags.append('italic')
530                        if special_text[1] == '*' and special_text[-2] == '*' and len(special_text) > 4: # it's also bold
531                                tags.append('bold')
532                                if not show_ascii_formatting_chars:
533                                        special_text = special_text[2:-2] # remove /* */
534                        elif special_text[1] == '_' and special_text[-2] == '_' and len(special_text) > 4: # it's also underlined
535                                tags.append('underline')
536                                if not show_ascii_formatting_chars:
537                                        special_text = special_text[2:-2] # remove /_ _/
538                        else:
539                                if not show_ascii_formatting_chars:
540                                        special_text = special_text[1:-1] # remove / /
541                elif special_text.startswith('_'): # it's an underlined text
542                        tags.append('underline')
543                        if special_text[1] == '*' and special_text[-2] == '*' and len(special_text) > 4: # it's also bold
544                                tags.append('bold')
545                                if not show_ascii_formatting_chars:
546  Â