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

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

[greblus] preferences window now can control the color of URLs

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