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

Revision 4737, 22.7 kB (checked in by nk, 3 years ago)

add catching for links and emots in history window

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