whiteboard plugin. added border to the main container
[gajim.git] / src / history_manager.py
blobce8f5841a6688190ae85e13474f932dfa41fe944
1 # -*- coding:utf-8 -*-
2 ## src/history_manager.py
3 ##
4 ## Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com>
5 ## Copyright (C) 2006-2007 Jean-Marie Traissard <jim AT lapin.org>
6 ## Nikos Kouremenos <kourem AT gmail.com>
7 ## Copyright (C) 2006-2010 Yann Leboulanger <asterix AT lagaule.org>
8 ## Copyright (C) 2007 Stephan Erb <steve-e AT h3c.de>
9 ## Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
11 ## This file is part of Gajim.
13 ## Gajim is free software; you can redistribute it and/or modify
14 ## it under the terms of the GNU General Public License as published
15 ## by the Free Software Foundation; version 3 only.
17 ## Gajim is distributed in the hope that it will be useful,
18 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 ## GNU General Public License for more details.
22 ## You should have received a copy of the GNU General Public License
23 ## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
26 ## NOTE: some method names may match those of logger.py but that's it
27 ## someday (TM) should have common class that abstracts db connections and helpers on it
28 ## the same can be said for history_window.py
30 import os
32 if os.name == 'nt':
33 import warnings
34 warnings.filterwarnings(action='ignore')
36 if os.path.isdir('gtk'):
37 # Used to create windows installer with GTK included
38 paths = os.environ['PATH']
39 list_ = paths.split(';')
40 new_list = []
41 for p in list_:
42 if p.find('gtk') < 0 and p.find('GTK') < 0:
43 new_list.append(p)
44 new_list.insert(0, 'gtk/lib')
45 new_list.insert(0, 'gtk/bin')
46 os.environ['PATH'] = ';'.join(new_list)
47 os.environ['GTK_BASEPATH'] = 'gtk'
49 import sys
50 import signal
51 import gtk
52 import gobject
53 import time
54 import locale
56 import getopt
57 from common import i18n
59 def parseOpts():
60 config_path = None
62 try:
63 shortargs = 'hc:'
64 longargs = 'help config_path='
65 opts = getopt.getopt(sys.argv[1:], shortargs, longargs.split())[0]
66 except getopt.error, msg:
67 print str(msg)
68 print 'for help use --help'
69 sys.exit(2)
70 for o, a in opts:
71 if o in ('-h', '--help'):
72 print 'history_manager [--help] [--config-path]'
73 sys.exit()
74 elif o in ('-c', '--config-path'):
75 config_path = a
76 return config_path
78 config_path = parseOpts()
79 del parseOpts
81 import common.configpaths
82 common.configpaths.gajimpaths.init(config_path)
83 del config_path
84 common.configpaths.gajimpaths.init_profile()
85 from common import exceptions
86 from common import gajim
87 import gtkgui_helpers
88 from common.logger import LOG_DB_PATH, constants
90 #FIXME: constants should implement 2 way mappings
91 status = dict((constants.__dict__[i], i[5:].lower()) for i in \
92 constants.__dict__.keys() if i.startswith('SHOW_'))
93 from common import helpers
94 import dialogs
96 # time, message, subject
98 C_UNIXTIME,
99 C_MESSAGE,
100 C_SUBJECT,
101 C_NICKNAME
102 ) = range(2, 6)
105 import sqlite3 as sqlite
108 class HistoryManager:
109 def __init__(self):
110 pix = gtkgui_helpers.get_icon_pixmap('gajim')
111 gtk.window_set_default_icon(pix) # set the icon to all newly opened windows
113 if not os.path.exists(LOG_DB_PATH):
114 dialogs.ErrorDialog(_('Cannot find history logs database'),
115 '%s does not exist.' % LOG_DB_PATH)
116 sys.exit()
118 xml = gtkgui_helpers.get_gtk_builder('history_manager.ui')
119 self.window = xml.get_object('history_manager_window')
120 self.jids_listview = xml.get_object('jids_listview')
121 self.logs_listview = xml.get_object('logs_listview')
122 self.search_results_listview = xml.get_object('search_results_listview')
123 self.search_entry = xml.get_object('search_entry')
124 self.logs_scrolledwindow = xml.get_object('logs_scrolledwindow')
125 self.search_results_scrolledwindow = xml.get_object(
126 'search_results_scrolledwindow')
127 self.welcome_vbox = xml.get_object('welcome_vbox')
129 self.jids_already_in = [] # holds jids that we already have in DB
130 self.AT_LEAST_ONE_DELETION_DONE = False
132 self.con = sqlite.connect(LOG_DB_PATH, timeout = 20.0,
133 isolation_level = 'IMMEDIATE')
134 self.cur = self.con.cursor()
136 self._init_jids_listview()
137 self._init_logs_listview()
138 self._init_search_results_listview()
140 self._fill_jids_listview()
142 self.search_entry.grab_focus()
144 self.window.show_all()
146 xml.connect_signals(self)
148 def _init_jids_listview(self):
149 self.jids_liststore = gtk.ListStore(str, str) # jid, jid_id
150 self.jids_listview.set_model(self.jids_liststore)
151 self.jids_listview.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
153 renderer_text = gtk.CellRendererText() # holds jid
154 col = gtk.TreeViewColumn(_('Contacts'), renderer_text, text = 0)
155 self.jids_listview.append_column(col)
157 self.jids_listview.get_selection().connect('changed',
158 self.on_jids_listview_selection_changed)
160 def _init_logs_listview(self):
161 # log_line_id (HIDDEN), jid_id (HIDDEN), time, message, subject, nickname
162 self.logs_liststore = gtk.ListStore(str, str, str, str, str, str)
163 self.logs_listview.set_model(self.logs_liststore)
164 self.logs_listview.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
166 renderer_text = gtk.CellRendererText() # holds time
167 col = gtk.TreeViewColumn(_('Date'), renderer_text, text = C_UNIXTIME)
168 col.set_sort_column_id(C_UNIXTIME) # user can click this header and sort
169 col.set_resizable(True)
170 self.logs_listview.append_column(col)
172 renderer_text = gtk.CellRendererText() # holds nickname
173 col = gtk.TreeViewColumn(_('Nickname'), renderer_text, text = C_NICKNAME)
174 col.set_sort_column_id(C_NICKNAME) # user can click this header and sort
175 col.set_resizable(True)
176 col.set_visible(False)
177 self.nickname_col_for_logs = col
178 self.logs_listview.append_column(col)
180 renderer_text = gtk.CellRendererText() # holds message
181 col = gtk.TreeViewColumn(_('Message'), renderer_text, markup = C_MESSAGE)
182 col.set_sort_column_id(C_MESSAGE) # user can click this header and sort
183 col.set_resizable(True)
184 self.message_col_for_logs = col
185 self.logs_listview.append_column(col)
187 renderer_text = gtk.CellRendererText() # holds subject
188 col = gtk.TreeViewColumn(_('Subject'), renderer_text, text = C_SUBJECT)
189 col.set_sort_column_id(C_SUBJECT) # user can click this header and sort
190 col.set_resizable(True)
191 col.set_visible(False)
192 self.subject_col_for_logs = col
193 self.logs_listview.append_column(col)
195 def _init_search_results_listview(self):
196 # log_line_id (HIDDEN), jid, time, message, subject, nickname
197 self.search_results_liststore = gtk.ListStore(str, str, str, str, str, str)
198 self.search_results_listview.set_model(self.search_results_liststore)
200 renderer_text = gtk.CellRendererText() # holds JID (who said this)
201 col = gtk.TreeViewColumn(_('JID'), renderer_text, text = 1)
202 col.set_sort_column_id(1) # user can click this header and sort
203 col.set_resizable(True)
204 self.search_results_listview.append_column(col)
206 renderer_text = gtk.CellRendererText() # holds time
207 col = gtk.TreeViewColumn(_('Date'), renderer_text, text = C_UNIXTIME)
208 col.set_sort_column_id(C_UNIXTIME) # user can click this header and sort
209 col.set_resizable(True)
210 self.search_results_listview.append_column(col)
212 renderer_text = gtk.CellRendererText() # holds message
213 col = gtk.TreeViewColumn(_('Message'), renderer_text, text = C_MESSAGE)
214 col.set_sort_column_id(C_MESSAGE) # user can click this header and sort
215 col.set_resizable(True)
216 self.search_results_listview.append_column(col)
218 renderer_text = gtk.CellRendererText() # holds subject
219 col = gtk.TreeViewColumn(_('Subject'), renderer_text, text = C_SUBJECT)
220 col.set_sort_column_id(C_SUBJECT) # user can click this header and sort
221 col.set_resizable(True)
222 self.search_results_listview.append_column(col)
224 renderer_text = gtk.CellRendererText() # holds nickname
225 col = gtk.TreeViewColumn(_('Nickname'), renderer_text, text = C_NICKNAME)
226 col.set_sort_column_id(C_NICKNAME) # user can click this header and sort
227 col.set_resizable(True)
228 self.search_results_listview.append_column(col)
230 def on_history_manager_window_delete_event(self, widget, event):
231 if self.AT_LEAST_ONE_DELETION_DONE:
232 def on_yes(clicked):
233 self.cur.execute('VACUUM')
234 self.con.commit()
235 gtk.main_quit()
237 def on_no():
238 gtk.main_quit()
240 dialogs.YesNoDialog(
241 _('Do you want to clean up the database? '
242 '(STRONGLY NOT RECOMMENDED IF GAJIM IS RUNNING)'),
243 _('Normally allocated database size will not be freed, '
244 'it will just become reusable. If you really want to reduce '
245 'database filesize, click YES, else click NO.'
246 '\n\nIn case you click YES, please wait...'),
247 on_response_yes=on_yes, on_response_no=on_no)
248 return
250 gtk.main_quit()
252 def _fill_jids_listview(self):
253 # get those jids that have at least one entry in logs
254 self.cur.execute('SELECT jid, jid_id FROM jids WHERE jid_id IN (SELECT '
255 'distinct logs.jid_id FROM logs) ORDER BY jid')
256 rows = self.cur.fetchall() # list of tupples: [(u'aaa@bbb',), (u'cc@dd',)]
257 for row in rows:
258 self.jids_already_in.append(row[0]) # jid
259 self.jids_liststore.append(row) # jid, jid_id
261 def on_jids_listview_selection_changed(self, widget, data = None):
262 liststore, list_of_paths = self.jids_listview.get_selection()\
263 .get_selected_rows()
264 paths_len = len(list_of_paths)
265 if paths_len == 0: # nothing is selected
266 return
268 self.logs_liststore.clear() # clear the store
270 self.welcome_vbox.hide()
271 self.search_results_scrolledwindow.hide()
272 self.logs_scrolledwindow.show()
274 list_of_rowrefs = []
275 for path in list_of_paths: # make them treerowrefs (it's needed)
276 list_of_rowrefs.append(gtk.TreeRowReference(liststore, path))
278 for rowref in list_of_rowrefs: # FILL THE STORE, for all rows selected
279 path = rowref.get_path()
280 if path is None:
281 continue
282 jid = liststore[path][0].decode('utf-8') # jid
283 self._fill_logs_listview(jid)
285 def _get_jid_id(self, jid):
287 jids table has jid and jid_id
288 logs table has log_id, jid_id, contact_name, time, kind, show, message
290 So to ask logs we need jid_id that matches our jid in jids table this
291 method wants jid and returns the jid_id for later sql-ing on logs
293 if jid.find('/') != -1: # if it has a /
294 jid_is_from_pm = self._jid_is_from_pm(jid)
295 if not jid_is_from_pm: # it's normal jid with resource
296 jid = jid.split('/', 1)[0] # remove the resource
297 self.cur.execute('SELECT jid_id FROM jids WHERE jid = ?', (jid,))
298 jid_id = self.cur.fetchone()[0]
299 return str(jid_id)
301 def _get_jid_from_jid_id(self, jid_id):
303 jids table has jid and jid_id
305 This method accepts jid_id and returns the jid for later sql-ing on logs
307 self.cur.execute('SELECT jid FROM jids WHERE jid_id = ?', (jid_id,))
308 jid = self.cur.fetchone()[0]
309 return jid
311 def _jid_is_from_pm(self, jid):
313 If jid is gajim@conf/nkour it's likely a pm one, how we know gajim@conf
314 is not a normal guy and nkour is not his resource? We ask if gajim@conf
315 is already in jids (with type room jid). This fails if user disables
316 logging for room and only enables for pm (so higly unlikely) and if we
317 fail we do not go chaos (user will see the first pm as if it was message
318 in room's public chat) and after that everything is ok
320 possible_room_jid = jid.split('/', 1)[0]
322 self.cur.execute('SELECT jid_id FROM jids WHERE jid = ? AND type = ?',
323 (possible_room_jid, constants.JID_ROOM_TYPE))
324 row = self.cur.fetchone()
325 if row is None:
326 return False
327 else:
328 return True
330 def _jid_is_room_type(self, jid):
332 Return True/False if given id is room type or not eg. if it is room
334 self.cur.execute('SELECT type FROM jids WHERE jid = ?', (jid,))
335 row = self.cur.fetchone()
336 if row is None:
337 raise
338 elif row[0] == constants.JID_ROOM_TYPE:
339 return True
340 else: # normal type
341 return False
343 def _fill_logs_listview(self, jid):
345 Fill the listview with all messages that user sent to or received from
348 # no need to lower jid in this context as jid is already lowered
349 # as we use those jids from db
350 jid_id = self._get_jid_id(jid)
351 self.cur.execute('''
352 SELECT log_line_id, jid_id, time, kind, message, subject, contact_name, show
353 FROM logs
354 WHERE jid_id = ?
355 ORDER BY time
356 ''', (jid_id,))
358 results = self.cur.fetchall()
360 if self._jid_is_room_type(jid): # is it room?
361 self.nickname_col_for_logs.set_visible(True)
362 self.subject_col_for_logs.set_visible(False)
363 else:
364 self.nickname_col_for_logs.set_visible(False)
365 self.subject_col_for_logs.set_visible(True)
367 for row in results:
368 # exposed in UI (TreeViewColumns) are only
369 # time, message, subject, nickname
370 # but store in liststore
371 # log_line_id, jid_id, time, message, subject, nickname
372 log_line_id, jid_id, time_, kind, message, subject, nickname, show = row
373 try:
374 time_ = time.strftime('%x', time.localtime(float(time_))).decode(
375 locale.getpreferredencoding())
376 except ValueError:
377 pass
378 else:
379 color = None
380 if kind in (constants.KIND_SINGLE_MSG_RECV,
381 constants.KIND_CHAT_MSG_RECV, constants.KIND_GC_MSG):
382 # it is the other side
383 color = gajim.config.get('inmsgcolor') # so incoming color
384 elif kind in (constants.KIND_SINGLE_MSG_SENT,
385 constants.KIND_CHAT_MSG_SENT): # it is us
386 color = gajim.config.get('outmsgcolor') # so outgoing color
387 elif kind in (constants.KIND_STATUS,
388 constants.KIND_GCSTATUS): # is is statuses
389 color = gajim.config.get('statusmsgcolor') # so status color
390 # include status into (status) message
391 if message is None:
392 message = ''
393 else:
394 message = ' : ' + message
395 message = helpers.get_uf_show(gajim.SHOW_LIST[show]) + message
397 message_ = '<span'
398 if color:
399 message_ += ' foreground="%s"' % color
400 message_ += '>%s</span>' % \
401 gobject.markup_escape_text(message)
402 self.logs_liststore.append((log_line_id, jid_id, time_, message_,
403 subject, nickname))
405 def _fill_search_results_listview(self, text):
407 Ask db and fill listview with results that match text
409 self.search_results_liststore.clear()
410 like_sql = '%' + text + '%'
411 self.cur.execute('''
412 SELECT log_line_id, jid_id, time, message, subject, contact_name
413 FROM logs
414 WHERE message LIKE ? OR subject LIKE ?
415 ORDER BY time
416 ''', (like_sql, like_sql))
418 results = self.cur.fetchall()
419 for row in results:
420 # exposed in UI (TreeViewColumns) are only
421 # JID, time, message, subject, nickname
422 # but store in liststore
423 # log_line_id, jid (from jid_id), time, message, subject, nickname
424 log_line_id, jid_id, time_, message, subject, nickname = row
425 try:
426 time_ = time.strftime('%x', time.localtime(float(time_))).decode(
427 locale.getpreferredencoding())
428 except ValueError:
429 pass
430 else:
431 jid = self._get_jid_from_jid_id(jid_id)
433 self.search_results_liststore.append((log_line_id, jid, time_,
434 message, subject, nickname))
436 def on_logs_listview_key_press_event(self, widget, event):
437 liststore, list_of_paths = self.logs_listview.get_selection()\
438 .get_selected_rows()
439 if event.keyval == gtk.keysyms.Delete:
440 self._delete_logs(liststore, list_of_paths)
442 def on_listview_button_press_event(self, widget, event):
443 if event.button == 3: # right click
444 xml = gtkgui_helpers.get_gtk_builder('history_manager.ui', 'context_menu')
445 if widget.name != 'jids_listview':
446 xml.get_object('export_menuitem').hide()
447 xml.get_object('delete_menuitem').connect('activate',
448 self.on_delete_menuitem_activate, widget)
450 xml.connect_signals(self)
451 xml.get_object('context_menu').popup(None, None, None,
452 event.button, event.time)
453 return True
455 def on_export_menuitem_activate(self, widget):
456 xml = gtkgui_helpers.get_gtk_builder('history_manager.ui', 'filechooserdialog')
457 xml.connect_signals(self)
459 dlg = xml.get_object('filechooserdialog')
460 dlg.set_title(_('Exporting History Logs...'))
461 dlg.set_current_folder(gajim.HOME_DIR)
462 dlg.props.do_overwrite_confirmation = True
463 response = dlg.run()
465 if response == gtk.RESPONSE_OK: # user want us to export ;)
466 liststore, list_of_paths = self.jids_listview.get_selection()\
467 .get_selected_rows()
468 path_to_file = dlg.get_filename()
469 self._export_jids_logs_to_file(liststore, list_of_paths, path_to_file)
471 dlg.destroy()
473 def on_delete_menuitem_activate(self, widget, listview):
474 widget_name = gtk.Buildable.get_name(listview)
475 liststore, list_of_paths = listview.get_selection().get_selected_rows()
476 if widget_name == 'jids_listview':
477 self._delete_jid_logs(liststore, list_of_paths)
478 elif widget_name in ('logs_listview', 'search_results_listview'):
479 self._delete_logs(liststore, list_of_paths)
480 else: # Huh ? We don't know this widget
481 return
483 def on_jids_listview_key_press_event(self, widget, event):
484 liststore, list_of_paths = self.jids_listview.get_selection()\
485 .get_selected_rows()
486 if event.keyval == gtk.keysyms.Delete:
487 self._delete_jid_logs(liststore, list_of_paths)
489 def _export_jids_logs_to_file(self, liststore, list_of_paths, path_to_file):
490 paths_len = len(list_of_paths)
491 if paths_len == 0: # nothing is selected
492 return
494 list_of_rowrefs = []
495 for path in list_of_paths: # make them treerowrefs (it's needed)
496 list_of_rowrefs.append(gtk.TreeRowReference(liststore, path))
498 for rowref in list_of_rowrefs:
499 path = rowref.get_path()
500 if path is None:
501 continue
502 jid_id = liststore[path][1]
503 self.cur.execute('''
504 SELECT time, kind, message, contact_name FROM logs
505 WHERE jid_id = ?
506 ORDER BY time
507 ''', (jid_id,))
509 # FIXME: we may have two contacts selected to export. fix that
510 # AT THIS TIME FIRST EXECUTE IS LOST! WTH!!!!!
511 results = self.cur.fetchall()
512 #print results[0]
513 file_ = open(path_to_file, 'w')
514 for row in results:
515 # in store: time, kind, message, contact_name FROM logs
516 # in text: JID or You or nickname (if it's gc_msg), time, message
517 time_, kind, message, nickname = row
518 if kind in (constants.KIND_SINGLE_MSG_RECV,
519 constants.KIND_CHAT_MSG_RECV):
520 who = self._get_jid_from_jid_id(jid_id)
521 elif kind in (constants.KIND_SINGLE_MSG_SENT,
522 constants.KIND_CHAT_MSG_SENT):
523 who = _('You')
524 elif kind == constants.KIND_GC_MSG:
525 who = nickname
526 else: # status or gc_status. do not save
527 #print kind
528 continue
530 try:
531 time_ = time.strftime('%c', time.localtime(float(time_))).decode(
532 locale.getpreferredencoding())
533 except ValueError:
534 pass
536 file_.write(_('%(who)s on %(time)s said: %(message)s\n') % {'who': who,
537 'time': time_, 'message': message})
539 def _delete_jid_logs(self, liststore, list_of_paths):
540 paths_len = len(list_of_paths)
541 if paths_len == 0: # nothing is selected
542 return
544 def on_ok(liststore, list_of_paths):
545 # delete all rows from db that match jid_id
546 list_of_rowrefs = []
547 for path in list_of_paths: # make them treerowrefs (it's needed)
548 list_of_rowrefs.append(gtk.TreeRowReference(liststore, path))
550 for rowref in list_of_rowrefs:
551 path = rowref.get_path()
552 if path is None:
553 continue
554 jid_id = liststore[path][1]
555 del liststore[path] # remove from UI
556 # remove from db
557 self.cur.execute('''
558 DELETE FROM logs
559 WHERE jid_id = ?
560 ''', (jid_id,))
562 # now delete "jid, jid_id" row from jids table
563 self.cur.execute('''
564 DELETE FROM jids
565 WHERE jid_id = ?
566 ''', (jid_id,))
568 self.con.commit()
570 self.AT_LEAST_ONE_DELETION_DONE = True
572 pri_text = i18n.ngettext(
573 'Do you really want to delete logs of the selected contact?',
574 'Do you really want to delete logs of the selected contacts?',
575 paths_len)
576 dialogs.ConfirmationDialog(pri_text,
577 _('This is an irreversible operation.'), on_response_ok = (on_ok,
578 liststore, list_of_paths))
580 def _delete_logs(self, liststore, list_of_paths):
581 paths_len = len(list_of_paths)
582 if paths_len == 0: # nothing is selected
583 return
585 def on_ok(liststore, list_of_paths):
586 # delete rows from db that match log_line_id
587 list_of_rowrefs = []
588 for path in list_of_paths: # make them treerowrefs (it's needed)
589 list_of_rowrefs.append(gtk.TreeRowReference(liststore, path))
591 for rowref in list_of_rowrefs:
592 path = rowref.get_path()
593 if path is None:
594 continue
595 log_line_id = liststore[path][0]
596 del liststore[path] # remove from UI
597 # remove from db
598 self.cur.execute('''
599 DELETE FROM logs
600 WHERE log_line_id = ?
601 ''', (log_line_id,))
603 self.con.commit()
605 self.AT_LEAST_ONE_DELETION_DONE = True
608 pri_text = i18n.ngettext(
609 'Do you really want to delete the selected message?',
610 'Do you really want to delete the selected messages?', paths_len)
611 dialogs.ConfirmationDialog(pri_text,
612 _('This is an irreversible operation.'), on_response_ok = (on_ok,
613 liststore, list_of_paths))
615 def on_search_db_button_clicked(self, widget):
616 text = self.search_entry.get_text().decode('utf-8')
617 if not text:
618 return
620 self.welcome_vbox.hide()
621 self.logs_scrolledwindow.hide()
622 self.search_results_scrolledwindow.show()
624 self._fill_search_results_listview(text)
626 def on_search_results_listview_row_activated(self, widget, path, column):
627 # get log_line_id, jid_id from row we double clicked
628 log_line_id = self.search_results_liststore[path][0]
629 jid = self.search_results_liststore[path][1].decode('utf-8')
630 # make it string as in gtk liststores I have them all as strings
631 # as this is what db returns so I don't have to fight with types
632 jid_id = self._get_jid_id(jid)
635 iter_ = self.jids_liststore.get_iter_root()
636 while iter_:
637 # self.jids_liststore[iter_][1] holds jid_ids
638 if self.jids_liststore[iter_][1] == jid_id:
639 break
640 iter_ = self.jids_liststore.iter_next(iter_)
642 if iter_ is None:
643 return
645 path = self.jids_liststore.get_path(iter_)
646 self.jids_listview.set_cursor(path)
648 iter_ = self.logs_liststore.get_iter_root()
649 while iter_:
650 # self.logs_liststore[iter_][0] holds lon_line_ids
651 if self.logs_liststore[iter_][0] == log_line_id:
652 break
653 iter_ = self.logs_liststore.iter_next(iter_)
655 path = self.logs_liststore.get_path(iter_)
656 self.logs_listview.scroll_to_cell(path)
658 if __name__ == '__main__':
659 signal.signal(signal.SIGINT, signal.SIG_DFL) # ^C exits the application
660 HistoryManager()
661 gtk.main()