history_window.py 30.9 KB
Newer Older
roidelapluie's avatar
roidelapluie committed
1
# -*- coding:utf-8 -*-
roidelapluie's avatar
roidelapluie committed
2
## src/history_window.py
3
##
Dicson's avatar
Dicson committed
4
## Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org>
roidelapluie's avatar
roidelapluie committed
5 6 7 8 9 10 11
## Copyright (C) 2005 Vincent Hanquez <tab AT snarc.org>
## Copyright (C) 2005-2006 Nikos Kouremenos <kourem AT gmail.com>
## Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com>
##                    Travis Shirk <travis AT pobox.com>
## Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
## Copyright (C) 2007-2008 Stephan Erb <steve-e AT h3c.de>
## Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
12
##
13 14 15
## This file is part of Gajim.
##
## Gajim is free software; you can redistribute it and/or modify
16
## it under the terms of the GNU General Public License as published
17
## by the Free Software Foundation; version 3 only.
18
##
19
## Gajim is distributed in the hope that it will be useful,
20
## but WITHOUT ANY WARRANTY; without even the implied warranty of
roidelapluie's avatar
roidelapluie committed
21
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 23
## GNU General Public License for more details.
##
24
## You should have received a copy of the GNU General Public License
roidelapluie's avatar
roidelapluie committed
25
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
26
##
27

28
from gi.repository import Gtk
Yann Leboulanger's avatar
Yann Leboulanger committed
29
from gi.repository import Gdk
Yann Leboulanger's avatar
Yann Leboulanger committed
30
from gi.repository import GLib
31
import time
32
import datetime
33

34
from enum import IntEnum, unique
35

André's avatar
André committed
36 37
from gajim import gtkgui_helpers
from gajim import conversation_textview
38
from gajim.gtk import ErrorDialog
39

40
from gajim.common import app
André's avatar
André committed
41 42
from gajim.common import helpers
from gajim.common import exceptions
43

Philipp Hörist's avatar
Philipp Hörist committed
44
from gajim.common.const import ShowConstant, KindConstant
45

46
@unique
47 48 49 50 51 52
class InfoColumn(IntEnum):
    '''Completion dict'''
    JID = 0
    ACCOUNT = 1
    NAME = 2
    COMPLETION = 3
steve-e's avatar
steve-e committed
53

54
@unique
55 56 57 58 59 60
class Column(IntEnum):
    LOG_JID = 0
    CONTACT_NAME = 1
    UNIXTIME = 2
    MESSAGE = 3
    TIME = 4
61
    LOG_LINE_ID = 5
62

63
class HistoryWindow:
64 65 66 67
    """
    Class for browsing logs of conversations with contacts
    """

68
    def __init__(self, jid=None, account=None):
69 70
        xml = gtkgui_helpers.get_gtk_builder('history_window.ui')
        self.window = xml.get_object('history_window')
71
        self.window.set_application(app.app)
72
        self.calendar = xml.get_object('calendar')
73 74 75 76
        self.button_first_day = xml.get_object('button_first_day')
        self.button_previous_day = xml.get_object('button_previous_day')
        self.button_next_day = xml.get_object('button_next_day')
        self.button_last_day = xml.get_object('button_last_day')
77 78
        scrolledwindow = xml.get_object('scrolledwindow')
        self.history_textview = conversation_textview.ConversationTextview(
Yann Leboulanger's avatar
Yann Leboulanger committed
79
            account, used_in_history_window = True)
80 81
        scrolledwindow.add(self.history_textview.tv)
        self.history_buffer = self.history_textview.tv.get_buffer()
82
        self.history_buffer.create_tag('highlight', background='yellow')
83
        self.history_buffer.create_tag('invisible', invisible=True)
84
        self.checkbutton = xml.get_object('log_history_checkbutton')
85
        self.show_status_checkbutton = xml.get_object('show_status_checkbutton')
Yann Leboulanger's avatar
Yann Leboulanger committed
86 87 88
        self.search_entry = xml.get_object('search_entry')
        self.query_liststore = xml.get_object('query_liststore')
        self.jid_entry = xml.get_object('query_entry')
89 90
        self.results_treeview = xml.get_object('results_treeview')
        self.results_window = xml.get_object('results_scrolledwindow')
91
        self.search_in_date = xml.get_object('search_in_date')
Sophie Herold's avatar
Sophie Herold committed
92 93 94 95
        self.date_label = xml.get_object('date_label')
        self.search_menu_button = xml.get_object('search_menu_button')

        self.clearing_search = False
96

97 98
        # jid, contact_name, date, message, time, log_line_id
        model = Gtk.ListStore(str, str, str, str, str, int)
99
        self.results_treeview.set_model(model)
100
        col = Gtk.TreeViewColumn(_('Name'))
101
        self.results_treeview.append_column(col)
102
        renderer = Gtk.CellRendererText()
Yann Leboulanger's avatar
Yann Leboulanger committed
103
        col.pack_start(renderer, True)
104 105
        col.add_attribute(renderer, 'text', Column.CONTACT_NAME)
        col.set_sort_column_id(Column.CONTACT_NAME) # user can click this header and sort
106 107
        col.set_resizable(True)

108
        col = Gtk.TreeViewColumn(_('Date'))
109
        self.results_treeview.append_column(col)
110
        renderer = Gtk.CellRendererText()
Yann Leboulanger's avatar
Yann Leboulanger committed
111
        col.pack_start(renderer, True)
112 113
        col.add_attribute(renderer, 'text', Column.UNIXTIME)
        col.set_sort_column_id(Column.UNIXTIME) # user can click this header and sort
114 115
        col.set_resizable(True)

116
        col = Gtk.TreeViewColumn(_('Message'))
117
        self.results_treeview.append_column(col)
118
        renderer = Gtk.CellRendererText()
Yann Leboulanger's avatar
Yann Leboulanger committed
119
        col.pack_start(renderer, True)
120
        col.add_attribute(renderer, 'text', Column.MESSAGE)
121 122 123
        col.set_resizable(True)

        self.jid = None # The history we are currently viewing
124
        self.account = account
125 126 127 128 129
        self.completion_dict = {}
        self.accounts_seen_online = [] # Update dict when new accounts connect
        self.jids_to_search = []

        # This will load history too
Yann Leboulanger's avatar
Yann Leboulanger committed
130
        task = self._fill_completion_dict()
Yann Leboulanger's avatar
Yann Leboulanger committed
131
        GLib.idle_add(next, task)
132 133

        if jid:
Sophie Herold's avatar
Sophie Herold committed
134
            self.jid_entry.get_child().set_text(jid)
135 136 137 138
        else:
            self._load_history(None)

        gtkgui_helpers.resize_window(self.window,
139 140
                app.config.get('history_window_width'),
                app.config.get('history_window_height'))
141
        gtkgui_helpers.move_window(self.window,
142 143
                app.config.get('history_window_x-position'),
                app.config.get('history_window_y-position'))
144 145 146 147

        xml.connect_signals(self)
        self.window.show_all()

148 149 150 151 152
        # PluginSystem: adding GUI extension point for
        # HistoryWindow instance object
        app.plugin_manager.gui_extension_point(
            'history_window', self)

153 154 155 156 157 158 159 160 161 162 163
    def _fill_completion_dict(self):
        """
        Fill completion_dict for key auto completion. Then load history for
        current jid (by calling another function)

        Key will be either jid or full_completion_name (contact name or long
        description like "pm-contact from groupchat....").

        {key : (jid, account, nick_name, full_completion_name}
        This is a generator and does pseudo-threading via idle_add().
        """
Sophie Herold's avatar
Sophie Herold committed
164 165 166 167 168 169
        liststore = gtkgui_helpers.get_completion_liststore(self.jid_entry.get_child())
        liststore.set_sort_column_id(1, Gtk.SortType.ASCENDING)
        self.jid_entry.get_child().get_completion().connect(
            'match-selected', self.on_jid_entry_match_selected)

        self.jid_entry.set_model(liststore)
170 171

        # Add all jids in logs.db:
172
        db_jids = app.logger.get_jids_in_db()
173 174
        completion_dict = dict.fromkeys(db_jids)

175
        self.accounts_seen_online = list(app.contacts.get_accounts())
176 177 178 179 180

        # Enhance contacts of online accounts with contact. Needed for mapping below
        for account in self.accounts_seen_online:
            completion_dict.update(helpers.get_contact_dict_for_account(account))

181 182
        muc_active_icon = gtkgui_helpers.get_iconset_name_for('muc-active')
        online_icon = gtkgui_helpers.get_iconset_name_for('online')
183

Yann Leboulanger's avatar
Yann Leboulanger committed
184
        keys = list(completion_dict.keys())
185
        # Move the actual jid at first so we load history faster
Sophie Herold's avatar
Sophie Herold committed
186
        actual_jid = self.jid_entry.get_child().get_text()
187 188 189
        if actual_jid in keys:
            keys.remove(actual_jid)
            keys.insert(0, actual_jid)
Yann Leboulanger's avatar
Yann Leboulanger committed
190 191
        if '' in keys:
            keys.remove('')
192 193
        if None in keys:
            keys.remove(None)
194 195 196 197
        # Map jid to info tuple
        # Warning : This for is time critical with big DB
        for key in keys:
            completed = key
198
            completed2 = None
199 200 201 202 203 204
            contact = completion_dict[completed]
            if contact:
                info_name = contact.get_shown_name()
                info_completion = info_name
                info_jid = contact.jid
            else:
Alexander Krotov's avatar
Alexander Krotov committed
205
                # Corresponding account is offline, we know nothing
206 207 208 209 210 211
                info_name = completed.split('@')[0]
                info_completion = completed
                info_jid = completed

            info_acc = self._get_account_for_jid(info_jid)

212 213
            if app.logger.jid_is_room_jid(completed) or\
            app.logger.jid_is_from_pm(completed):
214
                icon = muc_active_icon
215
                if app.logger.jid_is_from_pm(completed):
216
                    # It's PM. Make it easier to find
217
                    room, nick = app.get_room_and_nick_from_fjid(completed)
218 219
                    info_completion = '%s from %s' % (nick, room)
                    completed = info_completion
220 221
                    info_completion2 = '%s/%s' % (room, nick)
                    completed2 = info_completion2
222 223
                    info_name = nick
            else:
224
                icon = online_icon
225

226 227
            if len(completed) > 70:
                completed = completed[:70] + '[\u2026]'
228
            liststore.append((icon, completed))
229
            self.completion_dict[key] = (info_jid, info_acc, info_name,
230
                info_completion)
231
            self.completion_dict[completed] = (info_jid, info_acc,
232 233 234 235
                info_name, info_completion)
            if completed2:
                if len(completed2) > 70:
                    completed2 = completed2[:70] + '[\u2026]'
236
                liststore.append((icon, completed2))
237 238
                self.completion_dict[completed2] = (info_jid, info_acc,
                    info_name, info_completion2)
239
            if key == actual_jid:
240
                self._load_history(info_jid, self.account or info_acc)
241 242 243 244 245 246 247 248 249
            yield True
        keys.sort()
        yield False

    def _get_account_for_jid(self, jid):
        """
        Return the corresponding account of the jid. May be None if an account
        could not be found
        """
250
        accounts = app.contacts.get_accounts()
251 252
        account = None
        for acc in accounts:
253 254
            jid_list = app.contacts.get_jid_list(acc)
            gc_list = app.contacts.get_gc_list(acc)
255 256 257 258 259
            if jid in jid_list or jid in gc_list:
                account = acc
                break
        return account

Sophie Herold's avatar
Sophie Herold committed
260 261 262
    def on_history_window_delete_event(self, widget, *args):
        self.save_state()

263
    def on_history_window_destroy(self, widget):
264 265 266 267
        # PluginSystem: removing GUI extension points connected with
        # HistoryWindow instance object
        app.plugin_manager.remove_gui_extension_point(
            'history_window', self)
268
        self.history_textview.del_handlers()
269
        del app.interface.instances['logs']
270 271

    def on_history_window_key_press_event(self, widget, event):
272
        if event.keyval == Gdk.KEY_Escape:
273 274 275
            self.save_state()
            self.window.destroy()

Sophie Herold's avatar
Sophie Herold committed
276 277 278 279 280 281 282 283 284
    def on_jid_entry_match_selected(self, widget, model, iter_, *args):
        self._jid_entry_search(model[iter_][1])
        return True

    def on_jid_entry_changed(self, widget):
        # only if selected from combobox
        jid = self.jid_entry.get_child().get_text()
        if jid == self.jid_entry.get_active_id():
            self._jid_entry_search(jid)
285 286

    def on_jid_entry_activate(self, widget):
Sophie Herold's avatar
Sophie Herold committed
287 288 289
        self._jid_entry_search(self.jid_entry.get_child().get_text())

    def _jid_entry_search(self, jid):
290
        self._load_history(jid, self.account)
291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313
        self.results_window.set_property('visible', False)

    def _load_history(self, jid_or_name, account=None):
        """
        Load history for the given jid/name and show it
        """
        if jid_or_name and jid_or_name in self.completion_dict:
        # a full qualified jid or a contact name was entered
            info_jid, info_account, info_name, info_completion = self.completion_dict[jid_or_name]
            self.jids_to_search = [info_jid]
            self.jid = info_jid

            if account:
                self.account = account
            else:
                self.account = info_account
            if self.account is None:
                # We don't know account. Probably a gc not opened or an
                # account not connected.
                # Disable possibility to say if we want to log or not
                self.checkbutton.set_sensitive(False)
            else:
                # Are log disabled for account ?
314
                if self.account in app.config.get_per('accounts', self.account,
315 316 317 318 319 320
                        'no_log_for').split(' '):
                    self.checkbutton.set_active(False)
                    self.checkbutton.set_sensitive(False)
                else:
                    # Are log disabled for jid ?
                    log = True
321
                    if self.jid in app.config.get_per('accounts', self.account,
322 323 324 325 326 327 328
                            'no_log_for').split(' '):
                        log = False
                    self.checkbutton.set_active(log)
                    self.checkbutton.set_sensitive(True)

            self.jids_to_search = [info_jid]

329 330 331 332 333 334 335
            # Get first/last date we have logs with contact (for log navigation)
            self.first_log = app.logger.get_first_date_that_has_logs(
                self.account, self.jid)
            self.first_day = self._get_date_from_timestamp(self.first_log)
            self.last_log = app.logger.get_last_date_that_has_logs(
                self.account, self.jid)
            self.last_day = self._get_date_from_timestamp(self.last_log)
336

337
            # Select logs for last date we have logs with contact
Sophie Herold's avatar
Sophie Herold committed
338
            self.search_menu_button.set_sensitive(True)
339 340 341 342
            gtk_month = gtkgui_helpers.make_python_month_gtk_month(
                self.last_day.month)
            self.calendar.select_month(gtk_month, self.last_day.year)
            self.calendar.select_day(self.last_day.day)
343

344 345 346 347
            self.button_previous_day.set_sensitive(True)
            self.button_next_day.set_sensitive(True)
            self.button_first_day.set_sensitive(True)
            self.button_last_day.set_sensitive(True)
348

Yann Leboulanger's avatar
Yann Leboulanger committed
349 350
            self.search_entry.set_sensitive(True)
            self.search_entry.grab_focus()
351

Sophie Herold's avatar
Sophie Herold committed
352
            self.jid_entry.get_child().set_text(info_completion)
353 354 355 356 357 358 359

        else:   # neither a valid jid, nor an existing contact name was entered
            # we have got nothing to show or to search in
            self.jid = None
            self.account = None

            self.history_buffer.set_text('') # clear the buffer
Yann Leboulanger's avatar
Yann Leboulanger committed
360
            self.search_entry.set_sensitive(False)
361 362

            self.checkbutton.set_sensitive(False)
Sophie Herold's avatar
Sophie Herold committed
363
            self.search_menu_button.set_sensitive(False)
364
            self.calendar.clear_marks()
365 366 367 368
            self.button_previous_day.set_sensitive(False)
            self.button_next_day.set_sensitive(False)
            self.button_first_day.set_sensitive(False)
            self.button_last_day.set_sensitive(False)
369 370 371 372 373 374

            self.results_window.set_property('visible', False)

    def on_calendar_day_selected(self, widget):
        if not self.jid:
            return
375
        year, month, day = self.calendar.get_date() # integers
376
        month = gtkgui_helpers.make_gtk_month_python_month(month)
Sophie Herold's avatar
Sophie Herold committed
377 378
        date_str = datetime.date(year, month, day).strftime('%x')
        self.date_label.set_text(date_str)
379
        self._load_conversation(year, month, day)
380 381 382 383 384 385 386 387

    def on_calendar_month_changed(self, widget):
        """
        Ask for days in this month, if they have logs it bolds them (marks them)
        """
        if not self.jid:
            return
        year, month, day = widget.get_date() # integers
388 389 390 391 392
        if year < 1900:
            widget.select_month(0, 1900)
            widget.select_day(1)
            return

393 394
        widget.clear_marks()
        month = gtkgui_helpers.make_gtk_month_python_month(month)
395

396
        try:
397
            log_days = app.logger.get_days_with_logs(
398
                self.account, self.jid, year, month)
Yann Leboulanger's avatar
Yann Leboulanger committed
399
        except exceptions.PysqliteOperationalError as e:
400
            ErrorDialog(_('Disk Error'), str(e))
401
            return
402 403 404

        for date in log_days:
            widget.mark_day(date.day)
405

406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452
    def _get_date_from_timestamp(self, timestamp):
        # Conversion from timestamp to date
        log = time.localtime(timestamp)
        y, m, d = log[0], log[1], log[2]
        date = datetime.datetime(y, m, d)
        return(date)

    def _change_date(self, widget):
        # Get day selected in calendar
        y, m, d = self.calendar.get_date()
        py_m = gtkgui_helpers.make_gtk_month_python_month(m)
        _date = datetime.datetime(y, py_m, d)

        if widget is self.button_first_day:
            gtk_m = gtkgui_helpers.make_python_month_gtk_month(
                self.first_day.month)
            self.calendar.select_month(gtk_m, self.first_day.year)
            self.calendar.select_day(self.first_day.day)
            return
        elif widget is self.button_last_day:
            gtk_m = gtkgui_helpers.make_python_month_gtk_month(
                self.last_day.month)
            self.calendar.select_month(gtk_m, self.last_day.year)
            self.calendar.select_day(self.last_day.day)
            return
        elif widget is self.button_previous_day:
            end_date = self.first_day
            timedelta = datetime.timedelta(days=-1)
            if end_date >= _date:
                return
        elif widget is self.button_next_day:
            end_date = self.last_day
            timedelta = datetime.timedelta(days=1)
            if end_date <= _date:
                return

        # Iterate through days until log entry found or
        # supplied end_date (first_log / last_log) reached
        logs = None
        while logs is None:
            _date = _date + timedelta
            if _date == end_date:
                break
            try:
                logs = app.logger.get_date_has_logs(
                    self.account, self.jid, _date)
            except exceptions.PysqliteOperationalError as e:
453
                ErrorDialog(_('Disk Error'), str(e))
454 455 456 457 458 459
                return

        gtk_month = gtkgui_helpers.make_python_month_gtk_month(_date.month)
        self.calendar.select_month(gtk_month, _date.year)
        self.calendar.select_day(_date.day)

460
    def _get_string_show_from_constant_int(self, show):
461
        if show == ShowConstant.ONLINE:
462
            show = 'online'
463
        elif show == ShowConstant.CHAT:
464
            show = 'chat'
465
        elif show == ShowConstant.AWAY:
466
            show = 'away'
467
        elif show == ShowConstant.XA:
468
            show = 'xa'
469
        elif show == ShowConstant.DND:
470
            show = 'dnd'
471
        elif show == ShowConstant.OFFLINE:
472 473 474 475
            show = 'offline'

        return show

476
    def _load_conversation(self, year, month, day):
477
        """
478 479 480
        Load the conversation between `self.jid` and `self.account` held on the
        given date into the history textbuffer. Values for `month` and `day`
        are 1-based.
481
        """
482
        self.history_buffer.set_text('')
483
        self.last_time_printout = 0
484
        show_status = self.show_status_checkbutton.get_active()
485

486 487
        date = datetime.datetime(year, month, day)

488
        conversation = app.logger.get_conversation_for_date(
489 490
            self.account, self.jid, date)

491 492 493
        for message in conversation:
            if not show_status and message.kind in (KindConstant.GCSTATUS,
                                                    KindConstant.STATUS):
494
                continue
495
            self._add_message(message)
496

497 498 499
    def _add_message(self, msg):
        if not msg.message and msg.kind not in (KindConstant.STATUS,
                                                KindConstant.GCSTATUS):
500
            return
501

502 503 504 505 506 507 508 509 510
        tim = msg.time
        kind = msg.kind
        show = msg.show
        message = msg.message
        subject = msg.subject
        log_line_id = msg.log_line_id
        contact_name = msg.contact_name
        additional_data = msg.additional_data

511 512 513
        buf = self.history_buffer
        end_iter = buf.get_end_iter()

514 515 516
        # Make the beginning of every message searchable by its log_line_id
        buf.create_mark(str(log_line_id), end_iter, left_gravity=True)

517 518
        if app.config.get('print_time') == 'always':
            timestamp_str = app.config.get('time_stamp')
519 520
            timestamp_str = helpers.from_one_line(timestamp_str)
            tim = time.strftime(timestamp_str, time.localtime(float(tim)))
521
            buf.insert(end_iter, tim)
522 523
        elif app.config.get('print_time') == 'sometimes':
            every_foo_seconds = 60 * app.config.get(
524 525 526 527 528 529 530 531 532 533 534 535 536
                    'print_ichat_every_foo_minutes')
            seconds_passed = tim - self.last_time_printout
            if seconds_passed > every_foo_seconds:
                self.last_time_printout = tim
                tim = time.strftime('%X ', time.localtime(float(tim)))
                buf.insert_with_tags_by_name(end_iter, tim + '\n',
                        'time_sometimes')

        tag_name = ''
        tag_msg = ''

        show = self._get_string_show_from_constant_int(show)

537
        if kind == KindConstant.GC_MSG:
538
            tag_name = 'incoming'
539
        elif kind in (KindConstant.SINGLE_MSG_RECV, KindConstant.CHAT_MSG_RECV):
540
            contact_name = self.completion_dict[self.jid][InfoColumn.NAME]
541 542
            tag_name = 'incoming'
            tag_msg = 'incomingtxt'
543
        elif kind in (KindConstant.SINGLE_MSG_SENT, KindConstant.CHAT_MSG_SENT):
544
            if self.account:
545
                contact_name = app.nicks[self.account]
546 547 548
            else:
                # we don't have roster, we don't know our own nick, use first
                # account one (urk!)
549 550
                account = list(app.contacts.get_accounts())[0]
                contact_name = app.nicks[account]
551 552
            tag_name = 'outgoing'
            tag_msg = 'outgoingtxt'
553
        elif kind == KindConstant.GCSTATUS:
554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582
            # message here (if not None) is status message
            if message:
                message = _('%(nick)s is now %(status)s: %(status_msg)s') %\
                        {'nick': contact_name, 'status': helpers.get_uf_show(show),
                        'status_msg': message }
            else:
                message = _('%(nick)s is now %(status)s') % {'nick': contact_name,
                        'status': helpers.get_uf_show(show) }
            tag_msg = 'status'
        else: # 'status'
            # message here (if not None) is status message
            if show is None: # it means error
                if message:
                    message = _('Error: %s') % message
                else:
                    message = _('Error')
            elif message:
                message = _('Status is now: %(status)s: %(status_msg)s') % \
                        {'status': helpers.get_uf_show(show), 'status_msg': message}
            else:
                message = _('Status is now: %(status)s') % { 'status':
                        helpers.get_uf_show(show) }
            tag_msg = 'status'

        if message.startswith('/me ') or message.startswith('/me\n'):
            tag_msg = tag_name
        else:
            # do not do this if gcstats, avoid dupping contact_name
            # eg. nkour: nkour is now Offline
583
            if contact_name and kind != KindConstant.GCSTATUS:
584
                # add stuff before and after contact name
585
                before_str = app.config.get('before_nickname')
586
                before_str = helpers.from_one_line(before_str)
587
                after_str = app.config.get('after_nickname')
588 589
                after_str = helpers.from_one_line(after_str)
                format = before_str + contact_name + after_str + ' '
590 591 592 593
                if tag_name:
                    buf.insert_with_tags_by_name(end_iter, format, tag_name)
                else:
                    buf.insert(end_iter, format)
594 595
        if subject:
            message = _('Subject: %s\n') % subject + message
596 597 598 599
        xhtml = None
        if message.startswith('<body '):
            xhtml = message

600 601
        if tag_msg:
            self.history_textview.print_real_text(message, [tag_msg],
602
                    name=contact_name, xhtml=xhtml, additional_data=additional_data)
603
        else:
604
            self.history_textview.print_real_text(message, name=contact_name,
605
                xhtml=xhtml, additional_data=additional_data)
606
        self.history_textview.print_real_text('\n', text_tags=['eol'])
607

Sophie Herold's avatar
Sophie Herold committed
608 609 610 611 612 613
    def on_search_complete_history_toggled(self, widget):
        self.date_label.get_style_context().remove_class('tagged')

    def on_search_in_date_toggled(self, widget):
        self.date_label.get_style_context().add_class('tagged')

Yann Leboulanger's avatar
Yann Leboulanger committed
614 615
    def on_search_entry_activate(self, widget):
        text = self.search_entry.get_text()
Sophie Herold's avatar
Sophie Herold committed
616

617
        model = self.results_treeview.get_model()
Sophie Herold's avatar
Sophie Herold committed
618
        self.clearing_search = True
619
        model.clear()
Sophie Herold's avatar
Sophie Herold committed
620 621 622 623 624 625
        self.clearing_search = False

        start = self.history_buffer.get_start_iter()
        end = self.history_buffer.get_end_iter()
        self.history_buffer.remove_tag_by_name('highlight', start, end)

626 627 628 629 630 631 632
        if text == '':
            self.results_window.set_property('visible', False)
            return
        else:
            self.results_window.set_property('visible', True)

        # perform search in preselected jids
Yann Leboulanger's avatar
Yann Leboulanger committed
633
        # jids are preselected with the query_entry
634
        for jid in self.jids_to_search:
635
            account = self.completion_dict[jid][InfoColumn.ACCOUNT]
636 637 638 639 640
            if account is None:
                # We do not know an account. This can only happen if the contact is offine,
                # or if we browse a groupchat history. The account is not needed, a dummy can
                # be set.
                # This may leed to wrong self nick in the displayed history (Uggh!)
641
                account = list(app.contacts.get_accounts())[0]
642

Philipp Hörist's avatar
Philipp Hörist committed
643
            date = None
644 645 646
            if self.search_in_date.get_active():
                year, month, day = self.calendar.get_date() # integers
                month = gtkgui_helpers.make_gtk_month_python_month(month)
Philipp Hörist's avatar
Philipp Hörist committed
647
                date = datetime.datetime(year, month, day)
648

649
            show_status = self.show_status_checkbutton.get_active()
Philipp Hörist's avatar
Philipp Hörist committed
650

651
            results = app.logger.search_log(account, jid, text, date)
Sophie Herold's avatar
Sophie Herold committed
652
            result_found = False
653 654 655 656
            #FIXME:
            # add "subject:  | message: " in message column if kind is single
            # also do we need show at all? (we do not search on subject)
            for row in results:
657 658
                if not show_status and row.kind in (KindConstant.GCSTATUS,
                                                    KindConstant.STATUS):
659
                    continue
660 661

                contact_name = row.contact_name
662
                if not contact_name:
663
                    if row.kind == KindConstant.CHAT_MSG_SENT: # it's us! :)
664
                        contact_name = app.nicks[account]
665
                    else:
666
                        contact_name = self.completion_dict[jid][InfoColumn.NAME]
667 668

                local_time = time.localtime(row.time)
669 670
                date = time.strftime('%Y-%m-%d', local_time)

Sophie Herold's avatar
Sophie Herold committed
671
                result_found = True
672 673
                model.append((jid, contact_name, date, row.message,
                              str(row.time), row.log_line_id))
674

Sophie Herold's avatar
Sophie Herold committed
675 676 677 678
            if result_found:
                self.results_treeview.set_cursor(0)

    def on_results_treeview_cursor_changed(self, *args):
679
        """
Sophie Herold's avatar
Sophie Herold committed
680
        A row was selected, get date from row, and select it in calendar
681 682
        which results to showing conversation logs for that date
        """
Sophie Herold's avatar
Sophie Herold committed
683 684 685
        if self.clearing_search:
            return

686
        # get currently selected date
687
        cur_year, cur_month, cur_day = self.calendar.get_date()
688
        cur_month = gtkgui_helpers.make_gtk_month_python_month(cur_month)
Sophie Herold's avatar
Sophie Herold committed
689 690 691 692 693 694
        model, paths = self.results_treeview.get_selection().get_selected_rows()

        if not paths:
            return

        path = paths[0]
695
        # make it a tuple (Y, M, D, 0, 0, 0...)
696
        tim = time.strptime(model[path][Column.UNIXTIME], '%Y-%m-%d')
697 698 699 700 701 702
        year = tim[0]
        gtk_month = tim[1]
        month = gtkgui_helpers.make_python_month_gtk_month(gtk_month)
        day = tim[2]

        # switch to belonging logfile if necessary
703
        log_jid = model[path][Column.LOG_JID]
704 705 706 707 708 709 710
        if log_jid != self.jid:
            self._load_history(log_jid, None)

        # avoid reruning mark days algo if same month and year!
        if year != cur_year or gtk_month != cur_month:
            self.calendar.select_month(month, year)

711 712 713
        if year != cur_year or gtk_month != cur_month or day != cur_day:
            self.calendar.select_day(day)

714
        self._scroll_to_message_and_highlight(model[path][Column.LOG_LINE_ID])
715

716
    def _scroll_to_message_and_highlight(self, log_line_id):
717
        """
718
        Scroll to a message and highlight it
719
        """
720 721 722 723 724 725 726

        def iterator_has_mark(iterator, mark_name):
            for mark in iterator.get_marks():
                if mark.get_name() == mark_name:
                    return True
            return False

727 728 729 730 731 732
        # Clear previous search result by removing the highlighting. The scroll
        # mark is automatically removed when the new one is set.
        start = self.history_buffer.get_start_iter()
        end = self.history_buffer.get_end_iter()
        self.history_buffer.remove_tag_by_name('highlight', start, end)

733
        log_line_id = str(log_line_id)
734
        line = start
735 736 737 738 739 740 741 742 743 744 745
        while not iterator_has_mark(line, log_line_id):
            if not line.forward_line():
                return

        match_start = line
        match_end = match_start.copy()
        match_end.forward_to_tag_toggle(self.history_buffer.eol_tag)

        self.history_buffer.apply_tag_by_name('highlight', match_start, match_end)
        mark = self.history_buffer.create_mark('match', match_start, True)
        GLib.idle_add(self.history_textview.tv.scroll_to_mark, mark, 0, True, 0.0, 0.5)
746

Sophie Herold's avatar
Sophie Herold committed
747
    def on_log_history_checkbutton_toggled(self, widget, *args):
748 749
        # log conversation history?
        oldlog = True
750
        no_log_for = app.config.get_per('accounts', self.account,
751 752 753 754 755 756 757 758 759
                'no_log_for').split()
        if self.jid in no_log_for:
            oldlog = False
        log = widget.get_active()
        if not log and not self.jid in no_log_for:
            no_log_for.append(self.jid)
        if log and self.jid in no_log_for:
            no_log_for.remove(self.jid)
        if oldlog != log:
760
            app.config.set_per('accounts', self.account, 'no_log_for',
761 762
                    ' '.join(no_log_for))

763 764 765 766
    def on_show_status_checkbutton_toggled(self, widget):
        # reload logs
        self.on_calendar_day_selected(None)

767 768 769 770
    def open_history(self, jid, account):
        """
        Load chat history of the specified jid
        """
Sophie Herold's avatar
Sophie Herold committed
771
        self.jid_entry.get_child().set_text(jid)
772 773
        if account and account not in self.accounts_seen_online:
            # Update dict to not only show bare jid
Yann Leboulanger's avatar
Yann Leboulanger committed
774
            GLib.idle_add(next, self._fill_completion_dict())
775 776 777 778 779 780 781
        else:
            # Only in that case because it's called by self._fill_completion_dict()
            # otherwise
            self._load_history(jid, account)
        self.results_window.set_property('visible', False)

    def save_state(self):
Yann Leboulanger's avatar
Yann Leboulanger committed
782
        x, y = self.window.get_window().get_root_origin()
783 784
        width, height = self.window.get_size()

785 786 787 788
        app.config.set('history_window_x-position', x)
        app.config.set('history_window_y-position', y)
        app.config.set('history_window_width', width)
        app.config.set('history_window_height', height)