root/branches/gajim_0.11/src/history_manager.py

Revision 7940, 20.5 kB (checked in by asterix, 22 months ago)

merge diff from trunk

  • Property svn:executable set to *
Line 
1#!/usr/bin/env python
2## history_manager.py
3##
4## Copyright (C) 2006 Nikos Kouremenos <kourem@gmail.com>
5##
6## This program is free software; you can redistribute it and/or modify
7## it under the terms of the GNU General Public License as published
8## by the Free Software Foundation; version 2 only.
9##
10## This program is distributed in the hope that it will be useful,
11## but WITHOUT ANY WARRANTY; without even the implied warranty of
12## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13## GNU General Public License for more details.
14##
15
16## NOTE: some method names may match those of logger.py but that's it
17## someday (TM) should have common class that abstracts db connections and helpers on it
18## the same can be said for history_window.py
19
20import sys
21import os
22import signal
23import gtk
24import time
25import locale
26
27from common import i18n
28import exceptions
29import dialogs
30import gtkgui_helpers
31from common.logger import LOG_DB_PATH, constants
32
33#FIXME: constants should implement 2 way mappings
34status = dict((constants.__dict__[i], i[5:].lower()) for i in \
35        constants.__dict__.keys() if i.startswith('SHOW_')) 
36from common import gajim
37from common import helpers
38
39# time, message, subject
40(
41C_UNIXTIME,
42C_MESSAGE,
43C_SUBJECT,
44C_NICKNAME
45) = range(2, 6)
46
47
48try:
49        import sqlite3 as sqlite # python 2.5
50except ImportError:
51        try:
52                from pysqlite2 import dbapi2 as sqlite
53        except ImportError:
54                raise exceptions.PysqliteNotAvailable
55
56
57class HistoryManager:
58        def __init__(self):
59                path_to_file = os.path.join(gajim.DATA_DIR, 'pixmaps/gajim.png')
60                pix = gtk.gdk.pixbuf_new_from_file(path_to_file)
61                gtk.window_set_default_icon(pix) # set the icon to all newly opened windows
62               
63                if not os.path.exists(LOG_DB_PATH):
64                        dialogs.ErrorDialog(_('Cannot find history logs database'),
65                                '%s does not exist.' % LOG_DB_PATH)
66                        sys.exit()
67               
68                xml = gtkgui_helpers.get_glade('history_manager.glade')
69                self.window = xml.get_widget('history_manager_window')
70                self.jids_listview = xml.get_widget('jids_listview')
71                self.logs_listview = xml.get_widget('logs_listview')
72                self.search_results_listview = xml.get_widget('search_results_listview')
73                self.search_entry = xml.get_widget('search_entry')
74                self.logs_scrolledwindow = xml.get_widget('logs_scrolledwindow')
75                self.search_results_scrolledwindow = xml.get_widget(
76                        'search_results_scrolledwindow')
77                self.welcome_label = xml.get_widget('welcome_label')
78                       
79                self.logs_scrolledwindow.set_no_show_all(True)
80                self.search_results_scrolledwindow.set_no_show_all(True)
81               
82                self.jids_already_in = [] # holds jids that we already have in DB
83                self.AT_LEAST_ONE_DELETION_DONE = False
84               
85                self.con = sqlite.connect(LOG_DB_PATH, timeout = 20.0,
86                        isolation_level = 'IMMEDIATE')
87                self.cur = self.con.cursor()
88
89                self._init_jids_listview()
90                self._init_logs_listview()
91                self._init_search_results_listview()
92               
93                self._fill_jids_listview()
94               
95                self.search_entry.grab_focus()
96
97                self.window.show_all()
98               
99                xml.signal_autoconnect(self)
100       
101        def _init_jids_listview(self):
102                self.jids_liststore = gtk.ListStore(str, str) # jid, jid_id
103                self.jids_listview.set_model(self.jids_liststore)
104                self.jids_listview.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
105
106                renderer_text = gtk.CellRendererText() # holds jid
107                col = gtk.TreeViewColumn(_('Contacts'), renderer_text, text = 0)
108                self.jids_listview.append_column(col)
109               
110                self.jids_listview.get_selection().connect('changed',
111                        self.on_jids_listview_selection_changed)
112
113        def _init_logs_listview(self):
114                # log_line_id (HIDDEN), jid_id (HIDDEN), time, message, subject, nickname
115                self.logs_liststore = gtk.ListStore(str, str, str, str, str, str)
116                self.logs_listview.set_model(self.logs_liststore)
117                self.logs_listview.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
118
119                renderer_text = gtk.CellRendererText() # holds time
120                col = gtk.TreeViewColumn(_('Date'), renderer_text, text = C_UNIXTIME)
121                col.set_sort_column_id(C_UNIXTIME) # user can click this header and sort
122                col.set_resizable(True)
123                self.logs_listview.append_column(col)
124               
125                renderer_text = gtk.CellRendererText() # holds nickname
126                col = gtk.TreeViewColumn(_('Nickname'), renderer_text, text = C_NICKNAME)
127                col.set_sort_column_id(C_NICKNAME) # user can click this header and sort
128                col.set_resizable(True)
129                col.set_visible(False)
130                self.nickname_col_for_logs = col
131                self.logs_listview.append_column(col)
132
133                renderer_text = gtk.CellRendererText() # holds message
134                col = gtk.TreeViewColumn(_('Message'), renderer_text, markup = C_MESSAGE)
135                col.set_sort_column_id(C_MESSAGE) # user can click this header and sort
136                col.set_resizable(True)
137                self.message_col_for_logs = col
138                self.logs_listview.append_column(col)
139
140                renderer_text = gtk.CellRendererText() # holds subject
141                col = gtk.TreeViewColumn(_('Subject'), renderer_text, text = C_SUBJECT)
142                col.set_sort_column_id(C_SUBJECT) # user can click this header and sort
143                col.set_resizable(True)
144                col.set_visible(False)
145                self.subject_col_for_logs = col
146                self.logs_listview.append_column(col)
147
148        def _init_search_results_listview(self):
149                # log_line_id (HIDDEN), jid, time, message, subject, nickname
150                self.search_results_liststore = gtk.ListStore(str, str, str, str, str, str)
151                self.search_results_listview.set_model(self.search_results_liststore)
152               
153                renderer_text = gtk.CellRendererText() # holds JID (who said this)
154                col = gtk.TreeViewColumn(_('JID'), renderer_text, text = 1)
155                col.set_sort_column_id(1) # user can click this header and sort
156                col.set_resizable(True)
157                self.search_results_listview.append_column(col)
158               
159                renderer_text = gtk.CellRendererText() # holds time
160                col = gtk.TreeViewColumn(_('Date'), renderer_text, text = C_UNIXTIME)
161                col.set_sort_column_id(C_UNIXTIME) # user can click this header and sort
162                col.set_resizable(True)
163                self.search_results_listview.append_column(col)
164
165                renderer_text = gtk.CellRendererText() # holds message
166                col = gtk.TreeViewColumn(_('Message'), renderer_text, text = C_MESSAGE)
167                col.set_sort_column_id(C_MESSAGE) # user can click this header and sort
168                col.set_resizable(True)
169                self.search_results_listview.append_column(col)
170
171                renderer_text = gtk.CellRendererText() # holds subject
172                col = gtk.TreeViewColumn(_('Subject'), renderer_text, text = C_SUBJECT)
173                col.set_sort_column_id(C_SUBJECT) # user can click this header and sort
174                col.set_resizable(True)
175                self.search_results_listview.append_column(col)
176               
177                renderer_text = gtk.CellRendererText() # holds nickname
178                col = gtk.TreeViewColumn(_('Nickname'), renderer_text, text = C_NICKNAME)
179                col.set_sort_column_id(C_NICKNAME) # user can click this header and sort
180                col.set_resizable(True)
181                self.search_results_listview.append_column(col)
182       
183        def on_history_manager_window_delete_event(self, widget, event):
184                if self.AT_LEAST_ONE_DELETION_DONE:
185                        dialog = dialogs.YesNoDialog(
186                                _('Do you want to clean up the database? '
187                                '(STRONGLY NOT RECOMMENDED IF GAJIM IS RUNNING)'),
188                                _('Normally allocated database size will not be freed, '
189                                        'it will just become reusable. If you really want to reduce '
190                                        'database filesize, click YES, else click NO.'
191                                        '\n\nIn case you click YES, please wait...'))
192                        if dialog.get_response() == gtk.RESPONSE_YES:
193                                self.cur.execute('VACUUM')
194                                self.con.commit()
195                                                       
196                gtk.main_quit()
197       
198        def _fill_jids_listview(self):
199                # get those jids that have at least one entry in logs
200                self.cur.execute('SELECT jid, jid_id FROM jids WHERE jid_id IN (SELECT '
201                        'distinct logs.jid_id FROM logs) ORDER BY jid')
202                rows = self.cur.fetchall() # list of tupples: [(u'aaa@bbb',), (u'cc@dd',)]
203                for row in rows:
204                        self.jids_already_in.append(row[0]) # jid
205                        self.jids_liststore.append(row) # jid, jid_id
206       
207        def on_jids_listview_selection_changed(self, widget, data = None):
208                liststore, list_of_paths = self.jids_listview.get_selection()\
209                        .get_selected_rows()
210                paths_len = len(list_of_paths)
211                if paths_len == 0: # nothing is selected
212                        return
213
214                self.logs_liststore.clear() # clear the store
215               
216                self.welcome_label.hide()
217                self.search_results_scrolledwindow.hide()
218                self.logs_scrolledwindow.show()
219
220                list_of_rowrefs = []
221                for path in list_of_paths: # make them treerowrefs (it's needed)
222                        list_of_rowrefs.append(gtk.TreeRowReference(liststore, path))
223               
224                for rowref in list_of_rowrefs: # FILL THE STORE, for all rows selected
225                        path = rowref.get_path()
226                        if path is None:
227                                continue
228                        jid = liststore[path][0] # jid
229                        self._fill_logs_listview(jid)
230       
231        def _get_jid_id(self, jid):
232                '''jids table has jid and jid_id
233                logs table has log_id, jid_id, contact_name, time, kind, show, message
234                so to ask logs we need jid_id that matches our jid in jids table
235                this method wants jid and returns the jid_id for later sql-ing on logs
236                '''
237                if jid.find('/') != -1: # if it has a /
238                        jid_is_from_pm = self._jid_is_from_pm(jid)
239                        if not jid_is_from_pm: # it's normal jid with resource
240                                jid = jid.split('/', 1)[0] # remove the resource
241                self.cur.execute('SELECT jid_id FROM jids WHERE jid = ?', (jid,))
242                jid_id = self.cur.fetchone()[0]
243                return str(jid_id)
244
245        def _get_jid_from_jid_id(self, jid_id):
246                '''jids table has jid and jid_id
247                this method accepts jid_id and returns the jid for later sql-ing on logs
248                '''
249                self.cur.execute('SELECT jid FROM jids WHERE jid_id = ?', (jid_id,))
250                jid = self.cur.fetchone()[0]
251                return jid
252
253        def _jid_is_from_pm(self, jid):
254                '''if jid is gajim@conf/nkour it's likely a pm one, how we know
255                gajim@conf is not a normal guy and nkour is not his resource?
256                we ask if gajim@conf is already in jids (with type room jid)
257                this fails if user disables logging for room and only enables for
258                pm (so higly unlikely) and if we fail we do not go chaos
259                (user will see the first pm as if it was message in room's public chat)
260                and after that all okay'''
261               
262                possible_room_jid, possible_nick = jid.split('/', 1)
263               
264                self.cur.execute('SELECT jid_id FROM jids WHERE jid = ? AND type = ?',
265                        (possible_room_jid, constants.JID_ROOM_TYPE))
266                row = self.cur.fetchone()
267                if row is None:
268                        return False
269                else:
270                        return True
271
272        def _jid_is_room_type(self, jid):
273                '''returns True/False if given id is room type or not
274                eg. if it is room'''
275                self.cur.execute('SELECT type FROM jids WHERE jid = ?', (jid,))
276                row = self.cur.fetchone()
277                if row is None:
278                        raise
279                elif row[0] == constants.JID_ROOM_TYPE:
280                        return True
281                else: # normal type
282                        return False
283       
284        def _fill_logs_listview(self, jid):
285                '''fill the listview with all messages that user sent to or
286                received from JID'''
287                # no need to lower jid in this context as jid is already lowered
288                # as we use those jids from db
289                jid_id = self._get_jid_id(jid)
290                self.cur.execute('''
291                        SELECT log_line_id, jid_id, time, kind, message, subject, contact_name, show
292                        FROM logs
293                        WHERE jid_id = ?
294                        ORDER BY time
295                        ''', (jid_id,))
296
297                results = self.cur.fetchall()
298               
299                if self._jid_is_room_type(jid): # is it room?
300                        self.nickname_col_for_logs.set_visible(True)
301                        self.subject_col_for_logs.set_visible(False)
302                else:
303                        self.nickname_col_for_logs.set_visible(False)
304                        self.subject_col_for_logs.set_visible(True)
305
306                for row in results:
307                        # exposed in UI (TreeViewColumns) are only
308                        # time, message, subject, nickname
309                        # but store in liststore
310                        # log_line_id, jid_id, time, message, subject, nickname
311                        log_line_id, jid_id, time_, kind, message, subject, nickname, show = row
312                        try:
313                                time_ = time.strftime('%x', time.localtime(float(time_))).decode(
314                                        locale.getpreferredencoding())
315                        except ValueError:
316                                pass
317                        else:
318                                color = None
319                                if kind in (constants.KIND_SINGLE_MSG_RECV,
320                                constants.KIND_CHAT_MSG_RECV, constants.KIND_GC_MSG):
321                                        # it is the other side
322                                        color = gajim.config.get('inmsgcolor') # so incoming color
323                                elif kind in (constants.KIND_SINGLE_MSG_SENT,
324                                constants.KIND_CHAT_MSG_SENT): # it is us
325                                        color = gajim.config.get('outmsgcolor') # so outgoing color
326                                elif kind in (constants.KIND_STATUS,
327                                constants.KIND_GCSTATUS): # is is statuses
328                                        color = gajim.config.get('statusmsgcolor') # so status color
329                                        # include status into (status) message
330                                        if message is None:
331                                                message = ''
332                                        else:
333                                                message = ' : ' + message
334                                        message = helpers.get_uf_show(gajim.SHOW_LIST[show]) + message
335
336                                message_ = '<span'
337                                if color:
338                                        message_ += ' foreground="%s"' % color
339                                message_ += '>%s</span>' % \
340                                        gtkgui_helpers.escape_for_pango_markup(message)
341                                self.logs_liststore.append((log_line_id, jid_id, time_, message_,
342                                        subject, nickname))
343
344        def _fill_search_results_listview(self, text):
345                '''ask db and fill listview with results that match text'''
346                self.search_results_liststore.clear()
347                like_sql = '%' + text + '%'
348                self.cur.execute('''
349                        SELECT log_line_id, jid_id, time, message, subject, contact_name
350                        FROM logs
351                        WHERE message LIKE ? OR subject LIKE ?
352                        ORDER BY time
353                        ''', (like_sql, like_sql))
354               
355                results = self.cur.fetchall()
356                for row in results:
357                        # exposed in UI (TreeViewColumns) are only
358                        # JID, time, message, subject, nickname
359                        # but store in liststore
360                        # log_line_id, jid (from jid_id), time, message, subject, nickname
361                        log_line_id, jid_id, time_, message, subject, nickname = row
362                        try:
363                                time_ = time.strftime('%x', time.localtime(float(time_))).decode(
364                                        locale.getpreferredencoding())
365                        except ValueError:
366                                pass
367                        else:
368                                jid = self._get_jid_from_jid_id(jid_id)
369                               
370                                self.search_results_liststore.append((log_line_id, jid, time_,
371                                        message, subject, nickname))
372
373        def on_logs_listview_key_press_event(self, widget, event):
374                liststore, list_of_paths = self.logs_listview.get_selection()\
375                        .get_selected_rows()
376                if event.keyval == gtk.keysyms.Delete:
377                        self._delete_logs(liststore, list_of_paths)
378                       
379        def on_listview_button_press_event(self, widget, event):
380                if event.button == 3: # right click
381                        xml = gtkgui_helpers.get_glade('history_manager.glade', 'context_menu')
382                        if widget.name != 'jids_listview':
383                                xml.get_widget('export_menuitem').hide()
384                        xml.get_widget('delete_menuitem').connect('activate',
385                                self.on_delete_menuitem_activate, widget)
386                       
387                        liststore, list_of_paths = self.jids_listview.get_selection()\
388                                .get_selected_rows()
389                       
390                        xml.signal_autoconnect(self)
391                        xml.get_widget('context_menu').popup(None, None, None,
392                                event.button, event.time)
393                        return True
394
395        def on_export_menuitem_activate(self, widget):
396                xml = gtkgui_helpers.get_glade('history_manager.glade', 'filechooserdialog')
397                xml.signal_autoconnect(self)
398               
399                dlg = xml.get_widget('filechooserdialog')
400                dlg.set_title(_('Exporting History Logs...'))
401                dlg.set_current_folder(gajim.HOME_DIR)
402                if gtk.pygtk_version > (2, 8, 0):
403                        dlg.props.do_overwrite_confirmation = True
404                response = dlg.run()
405               
406                if response == gtk.RESPONSE_OK: # user want us to export ;)
407                        liststore, list_of_paths = self.jids_listview.get_selection()\
408                                .get_selected_rows()
409                        path_to_file = dlg.get_filename()
410                        self._export_jids_logs_to_file(liststore, list_of_paths, path_to_file)
411               
412                dlg.destroy()   
413       
414        def on_delete_menuitem_activate(self, widget, listview):
415                liststore, list_of_paths = listview.get_selection().get_selected_rows()
416                if listview.name == 'jids_listview':
417                        self._delete_jid_logs(liststore, list_of_paths)
418                elif listview.name in ('logs_listview', 'search_results_listview'):
419                        self._delete_logs(liststore, list_of_paths)
420                else: # Huh ? We don't know this widget
421                        return
422
423        def on_jids_listview_key_press_event(self, widget, event):
424                liststore, list_of_paths = self.jids_listview.get_selection()\
425                        .get_selected_rows()
426                if event.keyval == gtk.keysyms.Delete:
427                        self._delete_jid_logs(liststore, list_of_paths)
428
429        def _export_jids_logs_to_file(self, liststore, list_of_paths, path_to_file):
430                paths_len = len(list_of_paths)
431                if paths_len == 0: # nothing is selected
432                        return
433
434                list_of_rowrefs = []
435                for path in list_of_paths: # make them treerowrefs (it's needed)
436                        list_of_rowrefs.append(gtk.TreeRowReference(liststore, path))
437               
438                for rowref in list_of_rowrefs:
439                        path = rowref.get_path()
440                        if path is None:
441                                continue
442                        jid_id = liststore[path][1]
443                        self.cur.execute('''
444                                SELECT time, kind, message, contact_name FROM logs
445                                WHERE jid_id = ?
446                                ORDER BY time
447                                ''', (jid_id,))
448
449                # FIXME: we may have two contacts selected to export. fix that
450                # AT THIS TIME FIRST EXECUTE IS LOST! WTH!!!!!
451                results = self.cur.fetchall()
452                #print results[0]
453                file_ = open(path_to_file, 'w')
454                for row in results:
455                        # in store: time, kind, message, contact_name FROM logs
456                        # in text: JID or You or nickname (if it's gc_msg), time, message
457                        time_, kind, message, nickname = row
458                        if kind in (constants.KIND_SINGLE_MSG_RECV,
459                                constants.KIND_CHAT_MSG_RECV):
460                                who = self._get_jid_from_jid_id(jid_id)
461                        elif kind in (constants.KIND_SINGLE_MSG_SENT,
462                                constants.KIND_CHAT_MSG_SENT):
463                                who = _('You')
464                        elif kind == constants.KIND_GC_MSG:
465                                who = nickname
466                        else: # status or gc_status. do not save
467                                #print kind
468                                continue
469
470                        try:
471                                time_ = time.strftime('%x', time.localtime(float(time_))).decode(
472                                        locale.getpreferredencoding())
473                        except ValueError:
474                                pass
475
476                        file_.write(_('%(who)s on %(time)s said: %(message)s\n') % {'who': who,
477                                'time': time_, 'message': message})
478       
479        def _delete_jid_logs(self, liststore, list_of_paths):
480                paths_len = len(list_of_paths)
481                if paths_len == 0: # nothing is selected
482                        return
483
484                def on_ok(widget, liststore, list_of_paths):
485                        # delete all rows from db that match jid_id
486                        self.dialog.destroy()
487                        list_of_rowrefs = []
488                        for path in list_of_paths: # make them treerowrefs (it's needed)
489                                list_of_rowrefs.append(gtk.TreeRowReference(liststore, path))
490
491                        for rowref in list_of_rowrefs:
492                                path = rowref.get_path()
493                                if path is None:
494                                        continue
495                                jid_id = liststore[path][1]
496                                del liststore[path] # remove from UI
497                                # remove from db
498                                self.cur.execute('''
499                                        DELETE FROM logs
500                                        WHERE jid_id = ?
501                                        ''', (jid_id,))
502
503                                # now delete "jid, jid_id" row from jids table
504                                self.cur.execute('''
505                                                DELETE FROM jids
506                                                WHERE jid_id = ?
507                                                ''', (jid_id,))
508
509                        self.con.commit()
510
511                        self.AT_LEAST_ONE_DELETION_DONE = True
512
513                pri_text = i18n.ngettext(
514                        'Do you really want to delete logs of the selected contact?',
515                        'Do you really want to delete logs of the selected contacts?',
516                        paths_len)
517                self.dialog = dialogs.ConfirmationDialog(pri_text,
518                        _('This is an irreversible operation.'), on_response_ok = (on_ok,
519                        liststore, list_of_paths))
520
521        def _delete_logs(self, liststore, list_of_paths):
522                paths_len = len(list_of_paths)
523                if paths_len == 0: # nothing is selected
524                        return
525
526                def on_ok(widget, liststore, list_of_paths):
527                        self.dialog.destroy()
528                        # delete rows from db that match log_line_id
529                        list_of_rowrefs = []
530                        for path in list_of_paths: # make them treerowrefs (it's needed)
531                                list_of_rowrefs.append(gtk.TreeRowReference(liststore, path))
532
533                        for rowref in list_of_rowrefs:
534                                path = rowref.get_path()
535                                if path is None:
536                                        continue
537                                log_line_id = liststore[path][0]
538                                del liststore[path] # remove from UI
539                                # remove from db
540                                self.cur.execute('''
541                                        DELETE FROM logs
542                                        WHERE log_line_id = ?
543                                        ''', (log_line_id,))
544
545                        self.con.commit()
546
547                        self.AT_LEAST_ONE_DELETION_DONE = True
548
549                       
550                pri_text = i18n.ngettext(
551                        'Do you really want to delete the selected message?',
552                        'Do you really want to delete the selected messages?', paths_len)
553                self.dialog = dialogs.ConfirmationDialog(pri_text,
554                        _('This is an irreversible operation.'), on_response_ok = (on_ok,
555                        liststore, list_of_paths))
556
557        def on_search_db_button_clicked(self, widget):
558                text = self.search_entry.get_text()
559                if text == '':
560                        return
561
562                self.welcome_label.hide()
563                self.logs_scrolledwindow.hide()
564                self.search_results_scrolledwindow.show()
565               
566                self._fill_search_results_listview(text)
567
568        def on_search_results_listview_row_activated(self, widget, path, column):
569                # get log_line_id, jid_id from row we double clicked
570                log_line_id = self.search_results_liststore[path][0]
571                jid = self.search_results_liststore[path][1]
572                # make it string as in gtk liststores I have them all as strings
573                # as this is what db returns so I don't have to fight with types
574                jid_id = self._get_jid_id(jid)
575               
576               
577                iter_ = self.jids_liststore.get_iter_root()
578                while iter_:
579                        # self.jids_liststore[iter_][1] holds jid_ids
580                        if self.jids_liststore[iter_][1] == jid_id:
581                                break
582                        iter_ = self.jids_liststore.iter_next(iter_)
583               
584                if iter_ is None:
585                        return
586
587                path = self.jids_liststore.get_path(iter_)
588                self.jids_listview.set_cursor(path)
589               
590                iter_ = self.logs_liststore.get_iter_root()
591                while iter_:
592                        # self.logs_liststore[iter_][0] holds lon_line_ids
593                        if self.logs_liststore[iter_][0] == log_line_id:
594                                break
595                        iter_ = self.logs_liststore.iter_next(iter_)
596               
597                path = self.logs_liststore.get_path(iter_)
598                self.logs_listview.scroll_to_cell(path)
599
600if __name__ == '__main__':
601        signal.signal(signal.SIGINT, signal.SIG_DFL) # ^C exits the application
602        HistoryManager()
603        gtk.main()
Note: See TracBrowser for help on using the browser.