[kepi] ability to use subkeys. Fixes #6051
[gajim.git] / src / history_manager.py
blob755dc5359b842191a76977cb2b886e195b595a13
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
28 ## that abstracts db connections and helpers on it
29 ## the same can be said for history_window.py
31 import os
33 if os.name == 'nt':
34 import warnings
35 warnings.filterwarnings(action='ignore')
37 if os.path.isdir('gtk'):
38 # Used to create windows installer with GTK included
39 paths = os.environ['PATH']
40 list_ = paths.split(';')
41 new_list = []
42 for p in list_:
43 if p.find('gtk') < 0 and p.find('GTK') < 0:
44 new_list.append(p)
45 new_list.insert(0, 'gtk/lib')
46 new_list.insert(0, 'gtk/bin')
47 os.environ['PATH'] = ';'.join(new_list)
48 os.environ['GTK_BASEPATH'] = 'gtk'
50 import sys
51 import signal
52 import gtk
53 import gobject
54 import time
55 import locale
57 import getopt
58 from common import i18n
60 def parseOpts():
61 config_path = None
63 try:
64 shortargs = 'hc:'
65 longargs = 'help config_path='
66 opts = getopt.getopt(sys.argv[1:], shortargs, longargs.split())[0]
67 except getopt.error, msg:
68 print str(msg)
69 print 'for help use --help'
70 sys.exit(2)
71 for o, a in opts:
72 if o in ('-h', '--help'):
73 print 'history_manager [--help] [--config-path]'
74 sys.exit()
75 elif o in ('-c', '--config-path'):
76 config_path = a
77 return config_path
79 config_path = parseOpts()
80 del parseOpts
82 import common.configpaths
83 common.configpaths.gajimpaths.init(config_path)
84 del config_path
85 common.configpaths.gajimpaths.init_profile()
86 from common import exceptions
87 from common import gajim
88 import gtkgui_helpers
89 from common.logger import LOG_DB_PATH, constants
91 #FIXME: constants should implement 2 way mappings
92 status = dict((constants.__dict__[i], i[5:].lower()) for i in \
93 constants.__dict__.keys() if i.startswith('SHOW_'))
94 from common import helpers
95 import dialogs
97 # time, message, subject
99 C_UNIXTIME,
100 C_MESSAGE,
101 C_SUBJECT,
102 C_NICKNAME
103 ) = range(2, 6)
106 import sqlite3 as sqlite
109 class HistoryManager:
110 def __init__(self):
111 pix = gtkgui_helpers.get_icon_pixmap('gajim')
112 # set the icon to all newly opened windows
113 gtk.window_set_default_icon(pix)
115 if not os.path.exists(LOG_DB_PATH):
116 dialogs.ErrorDialog(_('Cannot find history logs database'),
117 '%s does not exist.' % LOG_DB_PATH)
118 sys.exit()
120 xml = gtkgui_helpers.get_gtk_builder('history_manager.ui')
121 self.window = xml.get_object('history_manager_window')
122 self.jids_listview = xml.get_object('jids_listview')
123 self.logs_listview = xml.get_object('logs_listview')
124 self.search_results_listview = xml.get_object('search_results_listview')
125 self.search_entry = xml.get_object('search_entry')
126 self.logs_scrolledwindow = xml.get_object('logs_scrolledwindow')
127 self.search_results_scrolledwindow = xml.get_object(
128 'search_results_scrolledwindow')
129 self.welcome_vbox = xml.get_object('welcome_vbox')
131 self.jids_already_in = [] # holds jids that we already have in DB
132 self.AT_LEAST_ONE_DELETION_DONE = False
134 self.con = sqlite.connect(LOG_DB_PATH, timeout=20.0,
135 isolation_level='IMMEDIATE')
136 self.cur = self.con.cursor()
138 self._init_jids_listview()
139 self._init_logs_listview()
140 self._init_search_results_listview()
142 self._fill_jids_listview()
144 self.search_entry.grab_focus()
146 self.window.show_all()
148 xml.connect_signals(self)
150 def _init_jids_listview(self):
151 self.jids_liststore = gtk.ListStore(str, str) # jid, jid_id
152 self.jids_listview.set_model(self.jids_liststore)
153 self.jids_listview.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
155 renderer_text = gtk.CellRendererText() # holds jid
156 col = gtk.TreeViewColumn(_('Contacts'), renderer_text, text=0)
157 self.jids_listview.append_column(col)
159 self.jids_listview.get_selection().connect('changed',
160 self.on_jids_listview_selection_changed)
162 def _init_logs_listview(self):
163 # log_line_id(HIDDEN), jid_id(HIDDEN), time, message, subject, nickname
164 self.logs_liststore = gtk.ListStore(str, str, str, str, str, str)
165 self.logs_listview.set_model(self.logs_liststore)
166 self.logs_listview.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
168 renderer_text = gtk.CellRendererText() # holds time
169 col = gtk.TreeViewColumn(_('Date'), renderer_text, text=C_UNIXTIME)
170 # user can click this header and sort
171 col.set_sort_column_id(C_UNIXTIME)
172 col.set_resizable(True)
173 self.logs_listview.append_column(col)
175 renderer_text = gtk.CellRendererText() # holds nickname
176 col = gtk.TreeViewColumn(_('Nickname'), renderer_text, text=C_NICKNAME)
177 # user can click this header and sort
178 col.set_sort_column_id(C_NICKNAME)
179 col.set_resizable(True)
180 col.set_visible(False)
181 self.nickname_col_for_logs = col
182 self.logs_listview.append_column(col)
184 renderer_text = gtk.CellRendererText() # holds message
185 col = gtk.TreeViewColumn(_('Message'), renderer_text, markup=C_MESSAGE)
186 # user can click this header and sort
187 col.set_sort_column_id(C_MESSAGE)
188 col.set_resizable(True)
189 self.message_col_for_logs = col
190 self.logs_listview.append_column(col)
192 renderer_text = gtk.CellRendererText() # holds subject
193 col = gtk.TreeViewColumn(_('Subject'), renderer_text, text=C_SUBJECT)
194 col.set_sort_column_id(C_SUBJECT) # user can click this header and sort
195 col.set_resizable(True)
196 col.set_visible(False)
197 self.subject_col_for_logs = col
198 self.logs_listview.append_column(col)
200 def _init_search_results_listview(self):
201 # log_line_id (HIDDEN), jid, time, message, subject, nickname
202 self.search_results_liststore = gtk.ListStore(str, str, str, str, str,
203 str)
204 self.search_results_listview.set_model(self.search_results_liststore)
206 renderer_text = gtk.CellRendererText() # holds JID (who said this)
207 col = gtk.TreeViewColumn(_('JID'), renderer_text, text=1)
208 col.set_sort_column_id(1) # 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 time
213 col = gtk.TreeViewColumn(_('Date'), renderer_text, text=C_UNIXTIME)
214 # user can click this header and sort
215 col.set_sort_column_id(C_UNIXTIME)
216 col.set_resizable(True)
217 self.search_results_listview.append_column(col)
219 renderer_text = gtk.CellRendererText() # holds message
220 col = gtk.TreeViewColumn(_('Message'), renderer_text, text=C_MESSAGE)
221 col.set_sort_column_id(C_MESSAGE) # user can click this header and sort
222 col.set_resizable(True)
223 self.search_results_listview.append_column(col)
225 renderer_text = gtk.CellRendererText() # holds subject
226 col = gtk.TreeViewColumn(_('Subject'), renderer_text, text=C_SUBJECT)
227 col.set_sort_column_id(C_SUBJECT) # user can click this header and sort
228 col.set_resizable(True)
229 self.search_results_listview.append_column(col)
231 renderer_text = gtk.CellRendererText() # holds nickname
232 col = gtk.TreeViewColumn(_('Nickname'), renderer_text, text=C_NICKNAME)
233 # user can click this header and sort
234 col.set_sort_column_id(C_NICKNAME)
235 col.set_resizable(True)
236 self.search_results_listview.append_column(col)
238 def on_history_manager_window_delete_event(self, widget, event):
239 if not self.AT_LEAST_ONE_DELETION_DONE:
240 gtk.main_quit()
241 return
243 def on_yes(clicked):
244 self.cur.execute('VACUUM')
245 self.con.commit()
246 gtk.main_quit()
248 def on_no():
249 gtk.main_quit()
251 dialog = dialogs.YesNoDialog(
252 _('Do you want to clean up the database? '
253 '(STRONGLY NOT RECOMMENDED IF GAJIM IS RUNNING)'),
254 _('Normally allocated database size will not be freed, '
255 'it will just become reusable. If you really want to reduce '
256 'database filesize, click YES, else click NO.'
257 '\n\nIn case you click YES, please wait...'),
258 on_response_yes=on_yes, on_response_no=on_no)
259 button_box = dialog.get_children()[0].get_children()[1]
260 button_box.get_children()[0].grab_focus()
262 def _fill_jids_listview(self):
263 # get those jids that have at least one entry in logs
264 self.cur.execute('SELECT jid, jid_id FROM jids WHERE jid_id IN ('
265 'SELECT distinct logs.jid_id FROM logs) ORDER BY jid')
266 # list of tupples: [(u'aaa@bbb',), (u'cc@dd',)]
267 rows = self.cur.fetchall()
268 for row in rows:
269 self.jids_already_in.append(row[0]) # jid
270 self.jids_liststore.append(row) # jid, jid_id
272 def on_jids_listview_selection_changed(self, widget, data=None):
273 liststore, list_of_paths = self.jids_listview.get_selection()\
274 .get_selected_rows()
275 paths_len = len(list_of_paths)
276 if paths_len == 0: # nothing is selected
277 return
279 self.logs_liststore.clear() # clear the store
281 self.welcome_vbox.hide()
282 self.search_results_scrolledwindow.hide()
283 self.logs_scrolledwindow.show()
285 list_of_rowrefs = []
286 for path in list_of_paths: # make them treerowrefs (it's needed)
287 list_of_rowrefs.append(gtk.TreeRowReference(liststore, path))
289 for rowref in list_of_rowrefs: # FILL THE STORE, for all rows selected
290 path = rowref.get_path()
291 if path is None:
292 continue
293 jid = liststore[path][0].decode('utf-8') # jid
294 self._fill_logs_listview(jid)
296 def _get_jid_id(self, jid):
298 jids table has jid and jid_id
299 logs table has log_id, jid_id, contact_name, time, kind, show, message
301 So to ask logs we need jid_id that matches our jid in jids table this
302 method wants jid and returns the jid_id for later sql-ing on logs
304 if jid.find('/') != -1: # if it has a /
305 jid_is_from_pm = self._jid_is_from_pm(jid)
306 if not jid_is_from_pm: # it's normal jid with resource
307 jid = jid.split('/', 1)[0] # remove the resource
308 self.cur.execute('SELECT jid_id FROM jids WHERE jid = ?', (jid,))
309 jid_id = self.cur.fetchone()[0]
310 return str(jid_id)
312 def _get_jid_from_jid_id(self, jid_id):
314 jids table has jid and jid_id
316 This method accepts jid_id and returns the jid for later sql-ing on logs
318 self.cur.execute('SELECT jid FROM jids WHERE jid_id = ?', (jid_id,))
319 jid = self.cur.fetchone()[0]
320 return jid
322 def _jid_is_from_pm(self, jid):
324 If jid is gajim@conf/nkour it's likely a pm one, how we know gajim@conf
325 is not a normal guy and nkour is not his resource? We ask if gajim@conf
326 is already in jids (with type room jid). This fails if user disables
327 logging for room and only enables for pm (so higly unlikely) and if we
328 fail we do not go chaos (user will see the first pm as if it was message
329 in room's public chat) and after that everything is ok
331 possible_room_jid = jid.split('/', 1)[0]
333 self.cur.execute('SELECT jid_id FROM jids WHERE jid = ? AND type = ?',
334 (possible_room_jid, constants.JID_ROOM_TYPE))
335 row = self.cur.fetchone()
336 if row is None:
337 return False
338 else:
339 return True
341 def _jid_is_room_type(self, jid):
343 Return True/False if given id is room type or not eg. if it is room
345 self.cur.execute('SELECT type FROM jids WHERE jid = ?', (jid,))
346 row = self.cur.fetchone()
347 if row is None:
348 raise
349 elif row[0] == constants.JID_ROOM_TYPE:
350 return True
351 else: # normal type
352 return False
354 def _fill_logs_listview(self, jid):
356 Fill the listview with all messages that user sent to or received from
359 # no need to lower jid in this context as jid is already lowered
360 # as we use those jids from db
361 jid_id = self._get_jid_id(jid)
362 self.cur.execute('''
363 SELECT log_line_id, jid_id, time, kind, message, subject, contact_name, show
364 FROM logs
365 WHERE jid_id = ?
366 ORDER BY time
367 ''', (jid_id,))
369 results = self.cur.fetchall()
371 if self._jid_is_room_type(jid): # is it room?
372 self.nickname_col_for_logs.set_visible(True)
373 self.subject_col_for_logs.set_visible(False)
374 else:
375 self.nickname_col_for_logs.set_visible(False)
376 self.subject_col_for_logs.set_visible(True)
378 for row in results:
379 # exposed in UI (TreeViewColumns) are only
380 # time, message, subject, nickname
381 # but store in liststore
382 # log_line_id, jid_id, time, message, subject, nickname
383 log_line_id, jid_id, time_, kind, message, subject, nickname, \
384 show = row
385 try:
386 time_ = time.strftime('%x', time.localtime(float(time_))
387 ).decode(locale.getpreferredencoding())
388 except ValueError:
389 pass
390 else:
391 color = None
392 if kind in (constants.KIND_SINGLE_MSG_RECV,
393 constants.KIND_CHAT_MSG_RECV, constants.KIND_GC_MSG):
394 # it is the other side
395 color = gajim.config.get('inmsgcolor') # so incoming color
396 elif kind in (constants.KIND_SINGLE_MSG_SENT,
397 constants.KIND_CHAT_MSG_SENT): # it is us
398 color = gajim.config.get('outmsgcolor') # so outgoing color
399 elif kind in (constants.KIND_STATUS,
400 constants.KIND_GCSTATUS): # is is statuses
401 # so status color
402 color = gajim.config.get('statusmsgcolor')
403 # include status into (status) message
404 if message is None:
405 message = ''
406 else:
407 message = ' : ' + message
408 message = helpers.get_uf_show(gajim.SHOW_LIST[show]) + \
409 message
411 message_ = '<span'
412 if color:
413 message_ += ' foreground="%s"' % color
414 message_ += '>%s</span>' % \
415 gobject.markup_escape_text(message)
416 self.logs_liststore.append((log_line_id, jid_id, time_,
417 message_, subject, nickname))
419 def _fill_search_results_listview(self, text):
421 Ask db and fill listview with results that match text
423 self.search_results_liststore.clear()
424 like_sql = '%' + text + '%'
425 self.cur.execute('''
426 SELECT log_line_id, jid_id, time, message, subject, contact_name
427 FROM logs
428 WHERE message LIKE ? OR subject LIKE ?
429 ORDER BY time
430 ''', (like_sql, like_sql))
432 results = self.cur.fetchall()
433 for row in results:
434 # exposed in UI (TreeViewColumns) are only
435 # JID, time, message, subject, nickname
436 # but store in liststore
437 # log_line_id, jid (from jid_id), time, message, subject, nickname
438 log_line_id, jid_id, time_, message, subject, nickname = row
439 try:
440 time_ = time.strftime('%x', time.localtime(float(time_))
441 ).decode(locale.getpreferredencoding())
442 except ValueError:
443 pass
444 else:
445 jid = self._get_jid_from_jid_id(jid_id)
447 self.search_results_liststore.append((log_line_id, jid, time_,
448 message, subject, nickname))
450 def on_logs_listview_key_press_event(self, widget, event):
451 liststore, list_of_paths = self.logs_listview.get_selection()\
452 .get_selected_rows()
453 if event.keyval == gtk.keysyms.Delete:
454 self._delete_logs(liststore, list_of_paths)
456 def on_listview_button_press_event(self, widget, event):
457 if event.button == 3: # right click
458 xml = gtkgui_helpers.get_gtk_builder('history_manager.ui',
459 'context_menu')
460 if widget.name != 'jids_listview':
461 xml.get_object('export_menuitem').hide()
462 xml.get_object('delete_menuitem').connect('activate',
463 self.on_delete_menuitem_activate, widget)
465 xml.connect_signals(self)
466 xml.get_object('context_menu').popup(None, None, None,
467 event.button, event.time)
468 return True
470 def on_export_menuitem_activate(self, widget):
471 xml = gtkgui_helpers.get_gtk_builder('history_manager.ui',
472 'filechooserdialog')
473 xml.connect_signals(self)
475 dlg = xml.get_object('filechooserdialog')
476 dlg.set_title(_('Exporting History Logs...'))
477 dlg.set_current_folder(gajim.HOME_DIR)
478 dlg.props.do_overwrite_confirmation = True
479 response = dlg.run()
481 if response == gtk.RESPONSE_OK: # user want us to export ;)
482 liststore, list_of_paths = self.jids_listview.get_selection()\
483 .get_selected_rows()
484 path_to_file = dlg.get_filename()
485 self._export_jids_logs_to_file(liststore, list_of_paths,
486 path_to_file)
488 dlg.destroy()
490 def on_delete_menuitem_activate(self, widget, listview):
491 widget_name = gtk.Buildable.get_name(listview)
492 liststore, list_of_paths = listview.get_selection().get_selected_rows()
493 if widget_name == 'jids_listview':
494 self._delete_jid_logs(liststore, list_of_paths)
495 elif widget_name in ('logs_listview', 'search_results_listview'):
496 self._delete_logs(liststore, list_of_paths)
497 else: # Huh ? We don't know this widget
498 return
500 def on_jids_listview_key_press_event(self, widget, event):
501 liststore, list_of_paths = self.jids_listview.get_selection()\
502 .get_selected_rows()
503 if event.keyval == gtk.keysyms.Delete:
504 self._delete_jid_logs(liststore, list_of_paths)
506 def _export_jids_logs_to_file(self, liststore, list_of_paths, path_to_file):
507 paths_len = len(list_of_paths)
508 if paths_len == 0: # nothing is selected
509 return
511 list_of_rowrefs = []
512 for path in list_of_paths: # make them treerowrefs (it's needed)
513 list_of_rowrefs.append(gtk.TreeRowReference(liststore, path))
515 for rowref in list_of_rowrefs:
516 path = rowref.get_path()
517 if path is None:
518 continue
519 jid_id = liststore[path][1]
520 self.cur.execute('''
521 SELECT time, kind, message, contact_name FROM logs
522 WHERE jid_id = ?
523 ORDER BY time
524 ''', (jid_id,))
526 # FIXME: we may have two contacts selected to export. fix that
527 # AT THIS TIME FIRST EXECUTE IS LOST! WTH!!!!!
528 results = self.cur.fetchall()
529 #print results[0]
530 file_ = open(path_to_file, 'w')
531 for row in results:
532 # in store: time, kind, message, contact_name FROM logs
533 # in text: JID or You or nickname (if it's gc_msg), time, message
534 time_, kind, message, nickname = row
535 if kind in (constants.KIND_SINGLE_MSG_RECV,
536 constants.KIND_CHAT_MSG_RECV):
537 who = self._get_jid_from_jid_id(jid_id)
538 elif kind in (constants.KIND_SINGLE_MSG_SENT,
539 constants.KIND_CHAT_MSG_SENT):
540 who = _('You')
541 elif kind == constants.KIND_GC_MSG:
542 who = nickname
543 else: # status or gc_status. do not save
544 #print kind
545 continue
547 try:
548 time_ = time.strftime('%c', time.localtime(float(time_))
549 ).decode(locale.getpreferredencoding())
550 except ValueError:
551 pass
553 file_.write(_('%(who)s on %(time)s said: %(message)s\n') % {
554 'who': who, 'time': time_, 'message': message})
556 def _delete_jid_logs(self, liststore, list_of_paths):
557 paths_len = len(list_of_paths)
558 if paths_len == 0: # nothing is selected
559 return
561 def on_ok(liststore, list_of_paths):
562 # delete all rows from db that match jid_id
563 list_of_rowrefs = []
564 for path in list_of_paths: # make them treerowrefs (it's needed)
565 list_of_rowrefs.append(gtk.TreeRowReference(liststore, path))
567 for rowref in list_of_rowrefs:
568 path = rowref.get_path()
569 if path is None:
570 continue
571 jid_id = liststore[path][1]
572 del liststore[path] # remove from UI
573 # remove from db
574 self.cur.execute('''
575 DELETE FROM logs
576 WHERE jid_id = ?
577 ''', (jid_id,))
579 # now delete "jid, jid_id" row from jids table
580 self.cur.execute('''
581 DELETE FROM jids
582 WHERE jid_id = ?
583 ''', (jid_id,))
585 self.con.commit()
587 self.AT_LEAST_ONE_DELETION_DONE = True
589 pri_text = i18n.ngettext(
590 'Do you really want to delete logs of the selected contact?',
591 'Do you really want to delete logs of the selected contacts?',
592 paths_len)
593 dialog = dialogs.ConfirmationDialog(pri_text,
594 _('This is an irreversible operation.'), on_response_ok=(on_ok,
595 liststore, list_of_paths))
596 ok_button = dialog.get_children()[0].get_children()[1].get_children()[0]
597 ok_button.grab_focus()
598 dialog.set_transient_for(self.window)
600 def _delete_logs(self, liststore, list_of_paths):
601 paths_len = len(list_of_paths)
602 if paths_len == 0: # nothing is selected
603 return
605 def on_ok(liststore, list_of_paths):
606 # delete rows from db that match log_line_id
607 list_of_rowrefs = []
608 for path in list_of_paths: # make them treerowrefs (it's needed)
609 list_of_rowrefs.append(gtk.TreeRowReference(liststore, path))
611 for rowref in list_of_rowrefs:
612 path = rowref.get_path()
613 if path is None:
614 continue
615 log_line_id = liststore[path][0]
616 del liststore[path] # remove from UI
617 # remove from db
618 self.cur.execute('''
619 DELETE FROM logs
620 WHERE log_line_id = ?
621 ''', (log_line_id,))
623 self.con.commit()
625 self.AT_LEAST_ONE_DELETION_DONE = True
627 pri_text = i18n.ngettext(
628 'Do you really want to delete the selected message?',
629 'Do you really want to delete the selected messages?', paths_len)
630 dialog = dialogs.ConfirmationDialog(pri_text,
631 _('This is an irreversible operation.'), on_response_ok=(on_ok,
632 liststore, list_of_paths))
633 ok_button = dialog.get_children()[0].get_children()[1].get_children()[0]
634 ok_button.grab_focus()
635 dialog.set_transient_for(self.window)
637 def on_search_db_button_clicked(self, widget):
638 text = self.search_entry.get_text().decode('utf-8')
639 if not text:
640 return
642 self.welcome_vbox.hide()
643 self.logs_scrolledwindow.hide()
644 self.search_results_scrolledwindow.show()
646 self._fill_search_results_listview(text)
648 def on_search_results_listview_row_activated(self, widget, path, column):
649 # get log_line_id, jid_id from row we double clicked
650 log_line_id = self.search_results_liststore[path][0]
651 jid = self.search_results_liststore[path][1].decode('utf-8')
652 # make it string as in gtk liststores I have them all as strings
653 # as this is what db returns so I don't have to fight with types
654 jid_id = self._get_jid_id(jid)
656 iter_ = self.jids_liststore.get_iter_root()
657 while iter_:
658 # self.jids_liststore[iter_][1] holds jid_ids
659 if self.jids_liststore[iter_][1] == jid_id:
660 break
661 iter_ = self.jids_liststore.iter_next(iter_)
663 if iter_ is None:
664 return
666 path = self.jids_liststore.get_path(iter_)
667 self.jids_listview.set_cursor(path)
669 iter_ = self.logs_liststore.get_iter_root()
670 while iter_:
671 # self.logs_liststore[iter_][0] holds lon_line_ids
672 if self.logs_liststore[iter_][0] == log_line_id:
673 break
674 iter_ = self.logs_liststore.iter_next(iter_)
676 path = self.logs_liststore.get_path(iter_)
677 self.logs_listview.scroll_to_cell(path)
679 if __name__ == '__main__':
680 signal.signal(signal.SIGINT, signal.SIG_DFL) # ^C exits the application
681 HistoryManager()
682 gtk.main()