| 1 | ## history_window.py |
|---|
| 2 | ## |
|---|
| 3 | ## Contributors for this file: |
|---|
| 4 | ## - Yann Le Boulanger <asterix@lagaule.org> |
|---|
| 5 | ## - Nikos Kouremenos <kourem@gmail.com> |
|---|
| 6 | ## |
|---|
| 7 | ## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org> |
|---|
| 8 | ## Vincent Hanquez <tab@snarc.org> |
|---|
| 9 | ## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org> |
|---|
| 10 | ## Vincent Hanquez <tab@snarc.org> |
|---|
| 11 | ## Nikos Kouremenos <nkour@jabber.org> |
|---|
| 12 | ## Dimitur Kirov <dkirov@gmail.com> |
|---|
| 13 | ## Travis Shirk <travis@pobox.com> |
|---|
| 14 | ## Norman Rasmussen <norman@rasmussen.co.za> |
|---|
| 15 | ## |
|---|
| 16 | ## This program is free software; you can redistribute it and/or modify |
|---|
| 17 | ## it under the terms of the GNU General Public License as published |
|---|
| 18 | ## by the Free Software Foundation; version 2 only. |
|---|
| 19 | ## |
|---|
| 20 | ## This program is distributed in the hope that it will be useful, |
|---|
| 21 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of |
|---|
| 22 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|---|
| 23 | ## GNU General Public License for more details. |
|---|
| 24 | ## |
|---|
| 25 | |
|---|
| 26 | import gtk |
|---|
| 27 | import gtk.glade |
|---|
| 28 | import gobject |
|---|
| 29 | import time |
|---|
| 30 | import calendar |
|---|
| 31 | |
|---|
| 32 | import gtkgui_helpers |
|---|
| 33 | import conversation_textview |
|---|
| 34 | |
|---|
| 35 | from common import gajim |
|---|
| 36 | from common import helpers |
|---|
| 37 | from common import i18n |
|---|
| 38 | |
|---|
| 39 | from common.logger import Constants |
|---|
| 40 | |
|---|
| 41 | constants = Constants() |
|---|
| 42 | |
|---|
| 43 | _ = i18n._ |
|---|
| 44 | APP = i18n.APP |
|---|
| 45 | gtk.glade.bindtextdomain(APP, i18n.DIR) |
|---|
| 46 | gtk.glade.textdomain(APP) |
|---|
| 47 | |
|---|
| 48 | GTKGUI_GLADE = 'gtkgui.glade' |
|---|
| 49 | |
|---|
| 50 | # contact_name, time, kind, show, message |
|---|
| 51 | ( |
|---|
| 52 | C_CONTACT_NAME, |
|---|
| 53 | C_TIME, |
|---|
| 54 | C_MESSAGE |
|---|
| 55 | ) = range(3) |
|---|
| 56 | |
|---|
| 57 | class HistoryWindow: |
|---|
| 58 | '''Class for browsing logs of conversations with contacts''' |
|---|
| 59 | |
|---|
| 60 | def __init__(self, jid, account): |
|---|
| 61 | self.jid = jid |
|---|
| 62 | self.account = account |
|---|
| 63 | self.mark_days_idle_call_id = None |
|---|
| 64 | |
|---|
| 65 | xml = gtk.glade.XML(GTKGUI_GLADE, 'history_window', APP) |
|---|
| 66 | self.window = xml.get_widget('history_window') |
|---|
| 67 | |
|---|
| 68 | self.calendar = xml.get_widget('calendar') |
|---|
| 69 | scrolledwindow = xml.get_widget('scrolledwindow') |
|---|
| 70 | self.history_textview = conversation_textview.ConversationTextview(account) |
|---|
| 71 | scrolledwindow.add(self.history_textview) |
|---|
| 72 | self.history_buffer = self.history_textview.get_buffer() |
|---|
| 73 | self.query_entry = xml.get_widget('query_entry') |
|---|
| 74 | self.search_button = xml.get_widget('search_button') |
|---|
| 75 | query_builder_button = xml.get_widget('query_builder_button') |
|---|
| 76 | query_builder_button.hide() |
|---|
| 77 | query_builder_button.set_no_show_all(True) |
|---|
| 78 | self.expander_vbox = xml.get_widget('expander_vbox') |
|---|
| 79 | |
|---|
| 80 | self.results_treeview = xml.get_widget('results_treeview') |
|---|
| 81 | # contact_name, time, message |
|---|
| 82 | model = gtk.ListStore(str, str, str) |
|---|
| 83 | self.results_treeview.set_model(model) |
|---|
| 84 | |
|---|
| 85 | col = gtk.TreeViewColumn(_('Name')) |
|---|
| 86 | self.results_treeview.append_column(col) |
|---|
| 87 | renderer = gtk.CellRendererText() |
|---|
| 88 | col.pack_start(renderer) |
|---|
| 89 | col.set_attributes(renderer, text = C_CONTACT_NAME) |
|---|
| 90 | col.set_sort_column_id(C_CONTACT_NAME) |
|---|
| 91 | col.set_resizable(True) |
|---|
| 92 | |
|---|
| 93 | col = gtk.TreeViewColumn(_('Date')) |
|---|
| 94 | self.results_treeview.append_column(col) |
|---|
| 95 | renderer = gtk.CellRendererText() |
|---|
| 96 | col.pack_start(renderer) |
|---|
| 97 | col.set_attributes(renderer, text = C_TIME) |
|---|
| 98 | col.set_sort_column_id(C_TIME) |
|---|
| 99 | col.set_resizable(True) |
|---|
| 100 | |
|---|
| 101 | col = gtk.TreeViewColumn(_('Message')) |
|---|
| 102 | self.results_treeview.append_column(col) |
|---|
| 103 | renderer = gtk.CellRendererText() |
|---|
| 104 | col.pack_start(renderer) |
|---|
| 105 | col.set_attributes(renderer, text = C_MESSAGE) |
|---|
| 106 | col.set_resizable(True) |
|---|
| 107 | |
|---|
| 108 | if account and gajim.contacts[account].has_key(jid): |
|---|
| 109 | contact = gajim.get_first_contact_instance_from_jid(account, jid) |
|---|
| 110 | title = _('Conversation History with %s') % contact.name |
|---|
| 111 | else: |
|---|
| 112 | title = _('Conversation History with %s') % jid |
|---|
| 113 | self.window.set_title(title) |
|---|
| 114 | |
|---|
| 115 | xml.signal_autoconnect(self) |
|---|
| 116 | |
|---|
| 117 | # fake event so we start mark days procedure for selected month |
|---|
| 118 | # selected month is current month as calendar defaults to selecting |
|---|
| 119 | # current date |
|---|
| 120 | self.calendar.emit('month-changed') |
|---|
| 121 | |
|---|
| 122 | # select and show logs for last date we have logs with contact |
|---|
| 123 | # and if we don't have logs at all, default to today |
|---|
| 124 | result = gajim.logger.get_last_date_that_has_logs(self.jid) |
|---|
| 125 | if result is None: |
|---|
| 126 | date = time.localtime() |
|---|
| 127 | else: |
|---|
| 128 | tim = result |
|---|
| 129 | date = time.localtime(tim) |
|---|
| 130 | |
|---|
| 131 | y, m, d = date[0], date[1], date[2] |
|---|
| 132 | gtk_month = gtkgui_helpers.make_python_month_gtk_month(m) |
|---|
| 133 | self.calendar.select_month(gtk_month, y) |
|---|
| 134 | self.calendar.select_day(d) |
|---|
| 135 | self.add_lines_for_date(y, m, d) |
|---|
| 136 | |
|---|
| 137 | self.window.show_all() |
|---|
| 138 | |
|---|
| 139 | def on_history_window_destroy(self, widget): |
|---|
| 140 | if self.mark_days_idle_call_id: |
|---|
| 141 | # if user destroys the window, and we have a generator filling mark days |
|---|
| 142 | # stop him! |
|---|
| 143 | gobject.source_remove(self.mark_days_idle_call_id) |
|---|
| 144 | del gajim.interface.instances['logs'][self.jid] |
|---|
| 145 | |
|---|
| 146 | def on_close_button_clicked(self, widget): |
|---|
| 147 | self.window.destroy() |
|---|
| 148 | |
|---|
| 149 | def on_calendar_day_selected(self, widget): |
|---|
| 150 | year, month, day = widget.get_date() # integers |
|---|
| 151 | month = gtkgui_helpers.make_gtk_month_python_month(month) |
|---|
| 152 | self.add_lines_for_date(year, month, day) |
|---|
| 153 | |
|---|
| 154 | def do_possible_mark_for_days_in_this_month(self, widget, year, month): |
|---|
| 155 | '''this is a generator and does pseudo-threading via idle_add() |
|---|
| 156 | so it runs progressively! yea :) |
|---|
| 157 | asks for days in this month if they have logs it bolds them (marks them)''' |
|---|
| 158 | weekday, days_in_this_month = calendar.monthrange(year, month) |
|---|
| 159 | log_days = gajim.logger.get_days_with_logs(self.jid, year, |
|---|
| 160 | month, days_in_this_month) |
|---|
| 161 | for day in log_days: |
|---|
| 162 | widget.mark_day(day) |
|---|
| 163 | yield True |
|---|
| 164 | yield False |
|---|
| 165 | |
|---|
| 166 | def on_calendar_month_changed(self, widget): |
|---|
| 167 | year, month, day = widget.get_date() # integers |
|---|
| 168 | # in gtk January is 1, in python January is 0, |
|---|
| 169 | # I want the second |
|---|
| 170 | # first day of month is 1 not 0 |
|---|
| 171 | if self.mark_days_idle_call_id: |
|---|
| 172 | # if user changed month, and we have a generator filling mark days |
|---|
| 173 | # stop him from marking dates for the previously selected month |
|---|
| 174 | gobject.source_remove(self.mark_days_idle_call_id) |
|---|
| 175 | widget.clear_marks() |
|---|
| 176 | month = gtkgui_helpers.make_gtk_month_python_month(month) |
|---|
| 177 | self.mark_days_idle_call_id = gobject.idle_add( |
|---|
| 178 | self.do_possible_mark_for_days_in_this_month(widget, year, month).next) |
|---|
| 179 | |
|---|
| 180 | def get_string_show_from_constant_int(self, show): |
|---|
| 181 | if show == constants.SHOW_ONLINE: |
|---|
| 182 | show = 'online' |
|---|
| 183 | elif show == constants.SHOW_CHAT: |
|---|
| 184 | show = 'chat' |
|---|
| 185 | elif show == constants.SHOW_AWAY: |
|---|
| 186 | show = 'away' |
|---|
| 187 | elif show == constants.SHOW_XA: |
|---|
| 188 | show = 'xa' |
|---|
| 189 | elif show == constants.SHOW_DND: |
|---|
| 190 | show = 'dnd' |
|---|
| 191 | elif show == constants.SHOW_OFFLINE: |
|---|
| 192 | show = 'offline' |
|---|
| 193 | |
|---|
| 194 | return show |
|---|
| 195 | |
|---|
| 196 | def add_lines_for_date(self, year, month, day): |
|---|
| 197 | '''adds all the lines for given date in textbuffer''' |
|---|
| 198 | self.history_buffer.set_text('') # clear the buffer first |
|---|
| 199 | self.last_time_printout = 0 |
|---|
| 200 | lines = gajim.logger.get_conversation_for_date(self.jid, year, month, day) |
|---|
| 201 | # lines holds list with tupples that have: |
|---|
| 202 | # contact_name, time, kind, show, message |
|---|
| 203 | for line in lines: |
|---|
| 204 | # line[0] is contact_name, line[1] is time of message |
|---|
| 205 | # line[2] is kind, line[3] is show, line[4] is message |
|---|
| 206 | self.add_new_line(line[0], line[1], line[2], line[3], line[4]) |
|---|
| 207 | |
|---|
| 208 | def add_new_line(self, contact_name, tim, kind, show, message): |
|---|
| 209 | '''add a new line in textbuffer''' |
|---|
| 210 | if not message: # None or '' |
|---|
| 211 | return |
|---|
| 212 | buf = self.history_buffer |
|---|
| 213 | end_iter = buf.get_end_iter() |
|---|
| 214 | |
|---|
| 215 | if gajim.config.get('print_time') == 'always': |
|---|
| 216 | before_str = gajim.config.get('before_time') |
|---|
| 217 | after_str = gajim.config.get('after_time') |
|---|
| 218 | format = before_str + '%X' + after_str + ' ' |
|---|
| 219 | tim = time.strftime(format, time.localtime(float(tim))) |
|---|
| 220 | buf.insert(end_iter, tim) # add time |
|---|
| 221 | elif gajim.config.get('print_time') == 'sometimes': |
|---|
| 222 | every_foo_seconds = 60 * gajim.config.get( |
|---|
| 223 | 'print_ichat_every_foo_minutes') |
|---|
| 224 | seconds_passed = tim - self.last_time_printout |
|---|
| 225 | if seconds_passed > every_foo_seconds: |
|---|
| 226 | self.last_time_printout = tim |
|---|
| 227 | tim = time.strftime('%X ', time.localtime(float(tim))) |
|---|
| 228 | buf.insert_with_tags_by_name(end_iter, tim + '\n', |
|---|
| 229 | 'time_sometimes') |
|---|
| 230 | |
|---|
| 231 | tag_name = '' |
|---|
| 232 | tag_msg = '' |
|---|
| 233 | |
|---|
| 234 | show = self.get_string_show_from_constant_int(show) |
|---|
| 235 | |
|---|
| 236 | if kind == constants.KIND_GC_MSG: |
|---|
| 237 | tag_name = 'incoming' |
|---|
| 238 | elif kind in (constants.KIND_SINGLE_MSG_RECV, constants.KIND_CHAT_MSG_RECV): |
|---|
| 239 | try: |
|---|
| 240 | # is he in our roster? if yes use the name |
|---|
| 241 | contact_name = gajim.contacts[self.account][self.jid][0].name |
|---|
| 242 | except: |
|---|
| 243 | room_jid, nick = gajim.get_room_and_nick_from_fjid(self.jid) |
|---|
| 244 | # do we have him as gc_contact? |
|---|
| 245 | if nick and gajim.gc_contacts[self.account].has_key(room_jid) and\ |
|---|
| 246 | gajim.gc_contacts[self.account][room_jid].has_key(nick): |
|---|
| 247 | # so yes, it's pm! |
|---|
| 248 | contact_name = nick |
|---|
| 249 | else: |
|---|
| 250 | contact_name = self.jid.split('@')[0] |
|---|
| 251 | tag_name = 'incoming' |
|---|
| 252 | elif kind in (constants.KIND_SINGLE_MSG_SENT, constants.KIND_CHAT_MSG_SENT): |
|---|
| 253 | contact_name = gajim.nicks[self.account] |
|---|
| 254 | tag_name = 'outgoing' |
|---|
| 255 | elif kind == constants.KIND_GCSTATUS: |
|---|
| 256 | # message here (if not None) is status message |
|---|
| 257 | if message: |
|---|
| 258 | message = _('%(nick)s is now %(status)s: %(status_msg)s') %\ |
|---|
| 259 | {'nick': contact_name, 'status': helpers.get_uf_show(show), |
|---|
| 260 | 'status_msg': message } |
|---|
| 261 | else: |
|---|
| 262 | message = _('%(nick)s is now %(status)s') % {'nick': contact_name, |
|---|
| 263 | 'status': helpers.get_uf_show(show) } |
|---|
| 264 | tag_msg = 'status' |
|---|
| 265 | else: # 'status' |
|---|
| 266 | # message here (if not None) is status message |
|---|
| 267 | if message: |
|---|
| 268 | message = _('Status is now: %(status)s: %(status_msg)s') % \ |
|---|
| 269 | {'status': helpers.get_uf_show(show), 'status_msg': message} |
|---|
| 270 | else: |
|---|
| 271 | message = _('Status is now: %(status)s') % { 'status': |
|---|
| 272 | helpers.get_uf_show(show) } |
|---|
| 273 | tag_msg = 'status' |
|---|
| 274 | |
|---|
| 275 | # do not do this if gcstats, avoid dupping contact_name |
|---|
| 276 | # eg. nkour: nkour is now Offline |
|---|
| 277 | if contact_name and kind != constants.KIND_GCSTATUS: |
|---|
| 278 | # add stuff before and after contact name |
|---|
| 279 | before_str = gajim.config.get('before_nickname') |
|---|
| 280 | after_str = gajim.config.get('after_nickname') |
|---|
| 281 | format = before_str + contact_name + after_str + ' ' |
|---|
| 282 | buf.insert_with_tags_by_name(end_iter, format, tag_name) |
|---|
| 283 | |
|---|
| 284 | message = message + '\n' |
|---|
| 285 | if tag_msg: |
|---|
| 286 | self.history_textview.print_real_text(message, [tag_msg]) |
|---|
| 287 | else: |
|---|
| 288 | self.history_textview.print_real_text(message) |
|---|
| 289 | |
|---|
| 290 | def set_unset_expand_on_expander(self, widget): |
|---|
| 291 | '''expander has to have expand to TRUE so scrolledwindow resizes properly |
|---|
| 292 | and does not have a static size. when expander is not expanded we set |
|---|
| 293 | expand property (note the Box one) to FALSE |
|---|
| 294 | to do this, we first get the box and then apply to expander widget |
|---|
| 295 | the True/False thingy depending if it's expanded or not |
|---|
| 296 | this function is called in a timeout just after expanded state changes''' |
|---|
| 297 | parent = widget.get_parent() # vbox |
|---|
| 298 | expanded = widget.get_expanded() |
|---|
| 299 | w, h = self.window.get_size() |
|---|
| 300 | if expanded: # resize to larger in height the window |
|---|
| 301 | self.window.resize(w, int(h*1.3)) |
|---|
| 302 | else: # resize to smaller in height the window |
|---|
| 303 | self.window.resize(w, int(h/1.3)) |
|---|
| 304 | # now set expand so if manually resizing scrolledwindow resizes too |
|---|
| 305 | parent.child_set_property(widget, 'expand', expanded) |
|---|
| 306 | |
|---|
| 307 | def on_search_expander_activate(self, widget): |
|---|
| 308 | if widget.get_expanded(): # it's the OPPOSITE!, it's not expanded |
|---|
| 309 | gobject.timeout_add(200, self.set_unset_expand_on_expander, widget) |
|---|
| 310 | else: |
|---|
| 311 | gobject.timeout_add(200, self.set_unset_expand_on_expander, widget) |
|---|
| 312 | self.search_button.grab_default() |
|---|
| 313 | self.query_entry.grab_focus() |
|---|
| 314 | |
|---|
| 315 | def on_search_button_clicked(self, widget): |
|---|
| 316 | text = self.query_entry.get_text() |
|---|
| 317 | model = self.results_treeview.get_model() |
|---|
| 318 | model.clear() |
|---|
| 319 | if text == '': |
|---|
| 320 | return |
|---|
| 321 | # contact_name, time, kind, show, message, subject |
|---|
| 322 | results = gajim.logger.get_search_results_for_query(self.jid, text) |
|---|
| 323 | #FIXME: investigate on kind and put name for normal chatting |
|---|
| 324 | #and add "subject: | message: " in message column is kind is |
|---|
| 325 | # single* |
|---|
| 326 | # also do we need show at all? |
|---|
| 327 | for row in results: |
|---|
| 328 | local_time = time.localtime(row[1]) |
|---|
| 329 | tim = time.strftime('%Y-%m-%d %H:%M:%S', local_time) |
|---|
| 330 | model.append((row[0], tim, row[4])) |
|---|
| 331 | |
|---|
| 332 | def on_results_treeview_row_activated(self, widget, path, column): |
|---|
| 333 | '''a row was double clicked, get date from row, and select it in calendar |
|---|
| 334 | which results to showing conversation logs for that date''' |
|---|
| 335 | # get currently selected date |
|---|
| 336 | cur_year, cur_month, cur_day = self.calendar.get_date() |
|---|
| 337 | cur_month = gtkgui_helpers.make_gtk_month_python_month(cur_month) |
|---|
| 338 | model = widget.get_model() |
|---|
| 339 | iter = model.get_iter(path) |
|---|
| 340 | # make it (Y, M, D, ...) |
|---|
| 341 | tim = time.strptime(model[iter][C_TIME], '%Y-%m-%d %H:%M:%S') |
|---|
| 342 | year = tim[0] |
|---|
| 343 | gtk_month = tim[1] |
|---|
| 344 | month = gtkgui_helpers.make_python_month_gtk_month(gtk_month) |
|---|
| 345 | day = tim[2] |
|---|
| 346 | |
|---|
| 347 | # avoid reruning mark days algo if same month and year! |
|---|
| 348 | if year != cur_year or gtk_month != cur_month: |
|---|
| 349 | self.calendar.select_month(month, year) |
|---|
| 350 | |
|---|
| 351 | self.calendar.select_day(day) |
|---|
| 352 | |
|---|
| 353 | # self.history_buffer.get_bounds() |
|---|
| 354 | #FIXME: start_iter.forward_search(string, TEXT_SEARCH_VISIBLE_ONLY, None) |
|---|
| 355 | # on double click and scroll there and maybe even highlight it |
|---|