root/trunk/src/history_window.py

Revision 10689, 20.8 kB (checked in by js, 8 days ago)

[kaylan] Remember size and position of history window. Closes #2824.

  • Property svn:eol-style set to LF
Line 
1# -*- coding:utf-8 -*-
2## src/history_window.py
3##
4## Copyright (C) 2003-2008 Yann Leboulanger <asterix AT lagaule.org>
5## Copyright (C) 2005 Vincent Hanquez <tab AT snarc.org>
6## Copyright (C) 2005-2006 Nikos Kouremenos <kourem AT gmail.com>
7## Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com>
8##                    Travis Shirk <travis AT pobox.com>
9## Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
10## Copyright (C) 2007-2008 Stephan Erb <steve-e AT h3c.de>
11## Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
12##
13## This file is part of Gajim.
14##
15## Gajim is free software; you can redistribute it and/or modify
16## it under the terms of the GNU General Public License as published
17## by the Free Software Foundation; version 3 only.
18##
19## Gajim is distributed in the hope that it will be useful,
20## but WITHOUT ANY WARRANTY; without even the implied warranty of
21## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22## GNU General Public License for more details.
23##
24## You should have received a copy of the GNU General Public License
25## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
26##
27
28import gtk
29import gobject
30import time
31import calendar
32
33import gtkgui_helpers
34import conversation_textview
35
36from common import gajim
37from common import helpers
38from common import exceptions
39
40from common.logger import Constants
41
42constants = Constants()
43
44# Completion dict
45(
46C_INFO_JID,
47C_INFO_ACCOUNT,
48C_INFO_NAME,
49C_INFO_COMPLETION
50) = range(4)
51
52# contact_name, date, message, time
53(
54C_LOG_JID,
55C_CONTACT_NAME,
56C_UNIXTIME,
57C_MESSAGE,
58C_TIME
59) = range(5)
60
61class HistoryWindow:
62        '''Class for browsing logs of conversations with contacts'''
63
64        def __init__(self, jid = None, account = None):
65                xml = gtkgui_helpers.get_glade('history_window.glade')
66                self.window = xml.get_widget('history_window')
67                self.jid_entry = xml.get_widget('jid_entry')
68                self.calendar = xml.get_widget('calendar')
69                scrolledwindow = xml.get_widget('scrolledwindow')
70                self.history_textview = conversation_textview.ConversationTextview(
71                        account, used_in_history_window = True)
72                scrolledwindow.add(self.history_textview.tv)
73                self.history_buffer = self.history_textview.tv.get_buffer()
74                self.history_buffer.create_tag('highlight', background = 'yellow')
75                self.checkbutton = xml.get_widget('log_history_checkbutton')
76                self.checkbutton.connect('toggled',
77                        self.on_log_history_checkbutton_toggled)
78                self.query_entry = xml.get_widget('query_entry')
79                self.query_combobox = xml.get_widget('query_combobox')
80                self.query_combobox.set_active(0)
81                self.results_treeview = xml.get_widget('results_treeview')
82                self.results_window = xml.get_widget('results_scrolledwindow')
83               
84                # contact_name, date, message, time
85                model = gtk.ListStore(str, str, str, str, str)
86                self.results_treeview.set_model(model)
87                col = gtk.TreeViewColumn(_('Name'))
88                self.results_treeview.append_column(col)
89                renderer = gtk.CellRendererText()
90                col.pack_start(renderer)
91                col.set_attributes(renderer, text = C_CONTACT_NAME)
92                col.set_sort_column_id(C_CONTACT_NAME) # user can click this header and sort
93                col.set_resizable(True)
94               
95                col = gtk.TreeViewColumn(_('Date'))
96                self.results_treeview.append_column(col)
97                renderer = gtk.CellRendererText()
98                col.pack_start(renderer)
99                col.set_attributes(renderer, text = C_UNIXTIME)
100                col.set_sort_column_id(C_UNIXTIME) # user can click this header and sort
101                col.set_resizable(True)
102               
103                col = gtk.TreeViewColumn(_('Message'))
104                self.results_treeview.append_column(col)
105                renderer = gtk.CellRendererText()
106                col.pack_start(renderer)
107                col.set_attributes(renderer, text = C_MESSAGE)
108                col.set_resizable(True)
109       
110                self.jid = None # The history we are currently viewing
111                self.account = None
112                self.completion_dict = {}
113                self.accounts_seen_online = [] # Update dict when new accounts connect
114                self.jids_to_search = []
115
116                # This will load history too
117                gobject.idle_add(self._fill_completion_dict().next)
118
119                if jid:
120                        self.jid_entry.set_text(jid)   
121
122                gtkgui_helpers.resize_window(self.window,
123                        gajim.config.get('history_window_width'),
124                        gajim.config.get('history_window_height'))
125                gtkgui_helpers.move_window(self.window,
126                        gajim.config.get('history_window_x-position'),
127                        gajim.config.get('history_window_y-position'))
128                               
129                xml.signal_autoconnect(self)
130                self.window.show_all()
131
132        def _fill_completion_dict(self):
133                '''Fill completion_dict for key auto completion. Then load history for
134                current jid (by calling another function).
135
136                Key will be either jid or full_completion_name 
137                (contact name or long description like "pm-contact from groupchat....")
138               
139                {key : (jid, account, nick_name, full_completion_name}
140                this is a generator and does pseudo-threading via idle_add()
141                '''
142                liststore = gtkgui_helpers.get_completion_liststore(self.jid_entry)
143
144                # Add all jids in logs.db:
145                db_jids = gajim.logger.get_jids_in_db()
146                self.completion_dict = dict.fromkeys(db_jids)
147               
148                self.accounts_seen_online = gajim.contacts.get_accounts()[:]
149
150                # Enhance contacts of online accounts with contact. Needed for mapping below
151                for account in self.accounts_seen_online:
152                        self.completion_dict.update(
153                                helpers.get_contact_dict_for_account(account))
154               
155                muc_active_img = gtkgui_helpers.load_icon('muc_active')
156                contact_img = gajim.interface.jabber_state_images['16']['online']
157                muc_active_pix = muc_active_img.get_pixbuf()
158                contact_pix = contact_img.get_pixbuf()
159               
160                keys = self.completion_dict.keys()
161                # Move the actual jid at first so we load history faster
162                actual_jid = self.jid_entry.get_text().decode('utf-8')
163                if actual_jid in keys:
164                        keys.remove(actual_jid)
165                        keys.insert(0, actual_jid)
166                if None in keys:
167                        keys.remove(None)
168                # Map jid to info tuple
169                # Warning : This for is time critical with big DB
170                for key in keys:
171                        completed = key
172                        contact = self.completion_dict[completed]
173                        if contact:
174                                info_name = contact.get_shown_name()
175                                info_completion = info_name
176                                info_jid = contact.jid
177                        else:
178                                # Corrensponding account is offline, we know nothing
179                                info_name = completed.split('@')[0]
180                                info_completion = completed
181                                info_jid = completed
182               
183                        info_acc = self._get_account_for_jid(info_jid)
184                       
185                        if gajim.logger.jid_is_room_jid(completed) or\
186                        gajim.logger.jid_is_from_pm(completed):
187                                pix = muc_active_pix
188                                if gajim.logger.jid_is_from_pm(completed):
189                                        # It's PM. Make it easier to find
190                                        room, nick = gajim.get_room_and_nick_from_fjid(completed)
191                                        info_completion = '%s from %s' % (nick, room)
192                                        completed = info_completion
193                                        info_name = nick
194                        else:
195                                pix = contact_pix
196
197                        liststore.append((pix, completed))
198                        self.completion_dict[key] = (info_jid, info_acc, info_name,
199                                info_completion)
200                        self.completion_dict[completed] = (info_jid, info_acc,
201                                info_name, info_completion)
202                        if key == actual_jid:
203                                self._load_history(info_jid, info_acc)
204                        yield True
205                keys.sort()
206                yield False
207       
208        def _get_account_for_jid(self, jid):
209                '''Return the corresponding account of the jid.
210                May be None if an account could not be found'''
211                accounts = gajim.contacts.get_accounts()
212                account = None
213                for acc in accounts:
214                        jid_list = gajim.contacts.get_jid_list(acc)
215                        gc_list = gajim.contacts.get_gc_list(acc)
216                        if jid in jid_list or jid in gc_list:
217                                account = acc
218                                break
219                return account
220
221        def on_history_window_destroy(self, widget):
222                self.history_textview.del_handlers()
223                del gajim.interface.instances['logs']
224
225        def on_history_window_key_press_event(self, widget, event):
226                if event.keyval == gtk.keysyms.Escape:
227                        self.save_state()
228                        self.window.destroy()
229
230        def on_close_button_clicked(self, widget):
231                self.save_state()
232                self.window.destroy()
233
234        def on_jid_entry_activate(self, widget):
235                if not self.query_combobox.get_active() < 0:
236                        # Don't disable querybox when we have changed the combobox
237                        # to GC or All and hit enter
238                        return
239                jid = self.jid_entry.get_text().decode('utf-8')
240                account = None # we don't know the account, could be any. Search for it!
241                self._load_history(jid, account)
242                self.results_window.set_property('visible', False)
243
244        def on_jid_entry_focus(self, widget, event):
245                        widget.select_region(0, -1) # select text
246
247        def _load_history(self, jid_or_name, account = None):
248                '''Load history for the given jid/name and show it''' 
249                if jid_or_name and jid_or_name in self.completion_dict: 
250                        # a full qualified jid or a contact name was entered
251                        info_jid, info_account, info_name, info_completion = self.completion_dict[jid_or_name]
252                        self.jids_to_search = [info_jid]
253                        self.jid = info_jid
254
255                        if account:
256                                self.account = account
257                        else:
258                                self.account = info_account
259                        if self.account is None: 
260                                # We don't know account. Probably a gc not opened or an
261                                # account not connected.
262                                # Disable possibility to say if we want to log or not
263                                self.checkbutton.set_sensitive(False)
264                        else:
265                                # Are log disabled for account ?
266                                if self.account in gajim.config.get_per('accounts', self.account,
267                                        'no_log_for').split(' '):
268                                        self.checkbutton.set_active(False)
269                                        self.checkbutton.set_sensitive(False)
270                                else:
271                                        # Are log disabled for jid ?
272                                        log = True
273                                        if self.jid in gajim.config.get_per('accounts', self.account,
274                                                'no_log_for').split(' '):
275                                                log = False
276                                        self.checkbutton.set_active(log)
277                                        self.checkbutton.set_sensitive(True)
278                       
279                        self.jids_to_search = [info_jid]
280
281                        # select logs for last date we have logs with contact
282                        self.calendar.set_sensitive(True)
283                        last_log = \
284                                gajim.logger.get_last_date_that_has_logs(self.jid, self.account)
285
286                        date = time.localtime(last_log)
287
288                        y, m, d = date[0], date[1], date[2]
289                        gtk_month = gtkgui_helpers.make_python_month_gtk_month(m)
290                        self.calendar.select_month(gtk_month, y)
291                        self.calendar.select_day(d)
292                       
293                        self.query_entry.set_sensitive(True)
294                        self.query_entry.grab_focus()
295
296                        title = _('Conversation History with %s') % info_name
297                        self.window.set_title(title)
298                        self.jid_entry.set_text(info_completion)
299
300                else:   # neither a valid jid, nor an existing contact name was entered
301                        # we have got nothing to show or to search in
302                        self.jid = None
303                        self.account = None
304                       
305                        self.history_buffer.set_text('') # clear the buffer
306                        self.query_entry.set_sensitive(False)
307
308                        self.checkbutton.set_sensitive(False)
309                        self.calendar.set_sensitive(False)
310                        self.calendar.clear_marks()
311
312                        self.results_window.set_property('visible', False)
313                       
314                        title = _('Conversation History')
315                        self.window.set_title(title)
316
317        def on_calendar_day_selected(self, widget):
318                if not self.jid:
319                        return
320                year, month, day = widget.get_date() # integers
321                month = gtkgui_helpers.make_gtk_month_python_month(month)
322                self._add_lines_for_date(year, month, day)
323               
324        def on_calendar_month_changed(self, widget):
325                '''asks for days in this month if they have logs it bolds them (marks
326                them)
327                '''
328                if not self.jid:
329                        return
330                year, month, day = widget.get_date() # integers
331                # in gtk January is 1, in python January is 0,
332                # I want the second
333                # first day of month is 1 not 0
334                widget.clear_marks()
335                month = gtkgui_helpers.make_gtk_month_python_month(month)
336                weekday, days_in_this_month = calendar.monthrange(year, month)
337                try:
338                        log_days = gajim.logger.get_days_with_logs(self.jid, year, month,
339                                days_in_this_month, self.account)
340                except exceptions.PysqliteOperationalError, e:
341                        dialogs.ErrorDialog(_('Disk Error'), str(e))
342                        return
343                for day in log_days:
344                        widget.mark_day(day)
345
346        def _get_string_show_from_constant_int(self, show):
347                if show == constants.SHOW_ONLINE:
348                        show = 'online'
349                elif show == constants.SHOW_CHAT:
350                        show = 'chat'
351                elif show == constants.SHOW_AWAY:
352                        show = 'away'
353                elif show == constants.SHOW_XA:
354                        show = 'xa'
355                elif show == constants.SHOW_DND:
356                        show = 'dnd'
357                elif show == constants.SHOW_OFFLINE:
358                        show = 'offline'
359
360                return show
361
362        def _add_lines_for_date(self, year, month, day):
363                '''adds all the lines for given date in textbuffer'''
364                self.history_buffer.set_text('') # clear the buffer first
365                self.last_time_printout = 0
366
367                lines = gajim.logger.get_conversation_for_date(self.jid, year, month, day, self.account)
368                # lines holds list with tupples that have:
369                # contact_name, time, kind, show, message
370                for line in lines:
371                        # line[0] is contact_name, line[1] is time of message
372                        # line[2] is kind, line[3] is show, line[4] is message
373                        self._add_new_line(line[0], line[1], line[2], line[3], line[4])
374       
375        def _add_new_line(self, contact_name, tim, kind, show, message):
376                '''add a new line in textbuffer'''
377                if not message and kind not in (constants.KIND_STATUS,
378                        constants.KIND_GCSTATUS):
379                        return
380                buf = self.history_buffer
381                end_iter = buf.get_end_iter()
382               
383                if gajim.config.get('print_time') == 'always':
384                        timestamp_str = gajim.config.get('time_stamp')
385                        timestamp_str = helpers.from_one_line(timestamp_str)
386                        tim = time.strftime(timestamp_str, time.localtime(float(tim)))
387                        buf.insert(end_iter, tim) # add time
388                elif gajim.config.get('print_time') == 'sometimes':
389                        every_foo_seconds = 60 * gajim.config.get(
390                                'print_ichat_every_foo_minutes')
391                        seconds_passed = tim - self.last_time_printout
392                        if seconds_passed > every_foo_seconds:
393                                self.last_time_printout = tim
394                                tim = time.strftime('%X ', time.localtime(float(tim)))
395                                buf.insert_with_tags_by_name(end_iter, tim + '\n',
396                                        'time_sometimes')
397
398                tag_name = ''
399                tag_msg = ''
400               
401                show = self._get_string_show_from_constant_int(show)
402               
403                if kind == constants.KIND_GC_MSG:
404                        tag_name = 'incoming'
405                elif kind in (constants.KIND_SINGLE_MSG_RECV,
406                constants.KIND_CHAT_MSG_RECV):
407                        contact_name = self.completion_dict[self.jid][C_INFO_NAME]
408                        tag_name = 'incoming'
409                elif kind in (constants.KIND_SINGLE_MSG_SENT,
410                constants.KIND_CHAT_MSG_SENT):
411                        if self.account:
412                                contact_name = gajim.nicks[self.account]
413                        else: 
414                                # we don't have roster, we don't know our own nick, use first
415                                # account one (urk!)
416                                account = gajim.contacts.get_accounts()[0] 
417                                contact_name = gajim.nicks[account]
418                        tag_name = 'outgoing'
419                elif kind == constants.KIND_GCSTATUS:
420                        # message here (if not None) is status message
421                        if message:
422                                message = _('%(nick)s is now %(status)s: %(status_msg)s') %\
423                                        {'nick': contact_name, 'status': helpers.get_uf_show(show),
424                                        'status_msg': message }
425                        else:
426                                message = _('%(nick)s is now %(status)s') % {'nick': contact_name,
427                                        'status': helpers.get_uf_show(show) }
428                        tag_msg = 'status'
429                else: # 'status'
430                        # message here (if not None) is status message
431                        if message:
432                                message = _('Status is now: %(status)s: %(status_msg)s') % \
433                                        {'status': helpers.get_uf_show(show), 'status_msg': message}
434                        else:
435                                message = _('Status is now: %(status)s') % { 'status':
436                                        helpers.get_uf_show(show) }
437                        tag_msg = 'status'
438
439                # do not do this if gcstats, avoid dupping contact_name
440                # eg. nkour: nkour is now Offline
441                if contact_name and kind != constants.KIND_GCSTATUS:
442                        # add stuff before and after contact name
443                        before_str = gajim.config.get('before_nickname')
444                        before_str = helpers.from_one_line(before_str)
445                        after_str = gajim.config.get('after_nickname')
446                        after_str = helpers.from_one_line(after_str)
447                        format = before_str + contact_name + after_str + ' '
448                        buf.insert_with_tags_by_name(end_iter, format, tag_name)
449
450                message = message + '\n'
451                if tag_msg:
452                        self.history_textview.print_real_text(message, [tag_msg])
453                else:
454                        self.history_textview.print_real_text(message)
455
456        def on_query_entry_activate(self, widget):
457                text = self.query_entry.get_text()
458                model = self.results_treeview.get_model()
459                model.clear()
460                if text == '':
461                        self.results_window.set_property('visible', False)     
462                        return
463                else:
464                        self.results_window.set_property('visible', True)
465
466                # perform search in preselected jids
467                # jids are preselected with the query_combobox (all, single jid...)
468                for jid in self.jids_to_search:
469                        account = self.completion_dict[jid][C_INFO_ACCOUNT]
470                        if account is None:
471                                # We do not know an account. This can only happen if the contact is offine,
472                                # or if we browse a groupchat history. The account is not needed, a dummy can
473                                # be set.
474                                # This may leed to wrong self nick in the displayed history (Uggh!)
475                                account = gajim.contacts.get_accounts()[0]
476
477                        # contact_name, time, kind, show, message, subject
478                        results = gajim.logger.get_search_results_for_query(
479                                                jid, text, account)
480                        #FIXME:
481                        # add "subject:  | message: " in message column if kind is single
482                        # also do we need show at all? (we do not search on subject)
483                        for row in results:
484                                contact_name = row[0]
485                                if not contact_name:
486                                        kind = row[2]
487                                        if kind == constants.KIND_CHAT_MSG_SENT: # it's us! :)
488                                                contact_name = gajim.nicks[account]
489                                        else:
490                                                contact_name = self.completion_dict[jid][C_INFO_NAME]
491                                tim = row[1]
492                                message = row[4]
493                                local_time = time.localtime(tim)
494                                date = time.strftime('%Y-%m-%d', local_time)
495
496                                #  jid (to which log is assigned to), name, date, message,
497                                # time (full unix time)
498                                model.append((jid, contact_name, date, message, tim))
499
500        def on_query_combobox_changed(self, widget):
501                if self.query_combobox.get_active() < 0:
502                        return # custom entry
503                self.account = None
504                self.jid = None
505                self.jids_to_search = []
506                self._load_history(None) # clear textview
507
508                if self.query_combobox.get_active() == 0:
509                        # JID or Contact name
510                        self.query_entry.set_sensitive(False)
511                        self.jid_entry.grab_focus()
512                if self.query_combobox.get_active() == 1:
513                        # Groupchat Histories
514                        self.query_entry.set_sensitive(True)
515                        self.query_entry.grab_focus()
516                        self.jids_to_search = (jid for jid in gajim.logger.get_jids_in_db() 
517                                        if gajim.logger.jid_is_room_jid(jid))
518                if self.query_combobox.get_active() == 2:
519                        # All Chat Histories
520                        self.query_entry.set_sensitive(True)
521                        self.query_entry.grab_focus()
522                        self.jids_to_search = gajim.logger.get_jids_in_db()
523                               
524        def on_results_treeview_row_activated(self, widget, path, column):
525                '''a row was double clicked, get date from row, and select it in calendar
526                which results to showing conversation logs for that date'''
527                # get currently selected date
528                cur_year, cur_month, cur_day = self.calendar.get_date()
529                cur_month = gtkgui_helpers.make_gtk_month_python_month(cur_month)
530                model = widget.get_model()
531                # make it a tupple (Y, M, D, 0, 0, 0...)
532                tim = time.strptime(model[path][C_UNIXTIME], '%Y-%m-%d')
533                year = tim[0]
534                gtk_month = tim[1]
535                month = gtkgui_helpers.make_python_month_gtk_month(gtk_month)
536                day = tim[2]
537                       
538                # switch to belonging logfile if necessary
539                log_jid = model[path][C_LOG_JID]
540                if log_jid != self.jid:
541                        self._load_history(log_jid, None)
542
543                # avoid reruning mark days algo if same month and year!
544                if year != cur_year or gtk_month != cur_month:
545                        self.calendar.select_month(month, year)
546               
547                self.calendar.select_day(day)
548                unix_time = model[path][C_TIME]
549                self._scroll_to_result(unix_time)
550                #FIXME: one day do not search just for unix_time but the whole and user
551                # specific format of the textbuffer line [time] nick: message
552                # and highlight all that
553
554        def _scroll_to_result(self, unix_time):
555                '''scrolls to the result using unix_time and highlight line'''
556                start_iter = self.history_buffer.get_start_iter()
557                local_time = time.localtime(float(unix_time))
558                tim = time.strftime('%X', local_time)
559                result = start_iter.forward_search(tim, gtk.TEXT_SEARCH_VISIBLE_ONLY,
560                        None)
561                if result is not None:
562                        match_start_iter, match_end_iter = result
563                        match_start_iter.backward_char() # include '[' or other character before time
564                        match_end_iter.forward_line() # highlight all message not just time
565                        self.history_buffer.apply_tag_by_name('highlight', match_start_iter,
566                                match_end_iter)
567                               
568                        match_start_mark = self.history_buffer.create_mark('match_start',
569                                match_start_iter, True)
570                        self.history_textview.tv.scroll_to_mark(match_start_mark, 0, True)
571
572        def on_log_history_checkbutton_toggled(self, widget):
573                # log conversation history?
574                oldlog = True
575                no_log_for = gajim.config.get_per('accounts', self.account,
576                        'no_log_for').split()
577                if self.jid in no_log_for:
578                        oldlog = False
579                log = widget.get_active()
580                if not log and not self.jid in no_log_for:
581                        no_log_for.append(self.jid)
582                if log and self.jid in no_log_for:
583                        no_log_for.remove(self.jid)
584                if oldlog != log:
585                        gajim.config.set_per('accounts', self.account, 'no_log_for',
586                                ' '.join(no_log_for))
587
588        def open_history(self, jid, account):
589                '''Load chat history of the specified jid'''
590                self.jid_entry.set_text(jid)
591                if account and account not in self.accounts_seen_online:
592                        # Update dict to not only show bare jid
593                        gobject.idle_add(self._fill_completion_dict().next)
594                else:
595                        # Only in that case because it's called by self._fill_completion_dict()
596                        # otherwise
597                        self._load_history(jid, account)
598                self.results_window.set_property('visible', False)
599
600        def save_state(self):
601                x,y = self.window.window.get_root_origin()
602                width, height = self.window.get_size()
603
604                gajim.config.set('history_window_x-position', x)
605                gajim.config.set('history_window_y-position', y)
606                gajim.config.set('history_window_width', width);
607                gajim.config.set('history_window_height', height);
608
609                gajim.interface.save_config()
610
611# vim: se ts=3:
Note: See TracBrowser for help on using the browser.