Rework message highlighting in the history window to not use timestamps

Search results in the history window to are scrolled to and highlighted
once the user clicks on one of them. Messages are searched by text in
the log database, but then looked up in the history text buffer via a
visible or invisible timestamp obtained from the database record.

Timestamps in the history text buffer, depending on user configuration,
may only have a coarse granularity. Philipp Hörist discovered that this
may lead to the highlighting of the wrong line in the history text view
and proposed an alternative solution that is implemented in this patch.

Timestamps are abolished as a means of searching messages. Instead, add
a mark with the unique message id (log line id) at the start of every
message in the history text buffer when the conversation is loaded.
After fetching the search results from the database this id can be used
to unambiguously find the corresponding message in the history text
buffer.
This commit is contained in:
Markus Böhme 2017-03-02 23:55:18 +01:00
parent 665898864f
commit d75ebd95e5
2 changed files with 43 additions and 38 deletions

View File

@ -628,7 +628,7 @@ class Logger:
def get_conversation_for_date(self, jid, year, month, day, account): def get_conversation_for_date(self, jid, year, month, day, account):
""" """
Return contact_name, time, kind, show, message, subject Return contact_name, time, kind, show, message, subject, additional_data, log_line_id
For each row in a list of tupples, returns list with empty tupple if we For each row in a list of tupples, returns list with empty tupple if we
found nothing to meet our demands found nothing to meet our demands
@ -645,7 +645,9 @@ class Logger:
last_second_of_day = start_of_day + seconds_in_a_day - 1 last_second_of_day = start_of_day + seconds_in_a_day - 1
self.cur.execute(''' self.cur.execute('''
SELECT contact_name, time, kind, show, message, subject, additional_data FROM logs SELECT contact_name, time, kind, show, message, subject,
additional_data, log_line_id
FROM logs
WHERE (%s) WHERE (%s)
AND time BETWEEN %d AND %d AND time BETWEEN %d AND %d
ORDER BY time ORDER BY time
@ -660,7 +662,7 @@ class Logger:
def get_search_results_for_query(self, jid, query, account, year=False, def get_search_results_for_query(self, jid, query, account, year=False,
month=False, day=False): month=False, day=False):
""" """
Returns contact_name, time, kind, show, message Returns contact_name, time, kind, show, message, subject, log_line_id
For each row in a list of tupples, returns list with empty tupple if we For each row in a list of tupples, returns list with empty tupple if we
found nothing to meet our demands found nothing to meet our demands
@ -678,7 +680,7 @@ class Logger:
seconds_in_a_day = 86400 # 60 * 60 * 24 seconds_in_a_day = 86400 # 60 * 60 * 24
last_second_of_day = start_of_day + seconds_in_a_day - 1 last_second_of_day = start_of_day + seconds_in_a_day - 1
self.cur.execute(''' self.cur.execute('''
SELECT contact_name, time, kind, show, message, subject FROM logs SELECT contact_name, time, kind, show, message, subject, log_line_id FROM logs
WHERE (%s) AND message LIKE '%s' WHERE (%s) AND message LIKE '%s'
AND time BETWEEN %d AND %d AND time BETWEEN %d AND %d
ORDER BY time ORDER BY time
@ -686,7 +688,7 @@ class Logger:
jid_tuple) jid_tuple)
else: else:
self.cur.execute(''' self.cur.execute('''
SELECT contact_name, time, kind, show, message, subject FROM logs SELECT contact_name, time, kind, show, message, subject, log_line_id FROM logs
WHERE (%s) AND message LIKE '%s' WHERE (%s) AND message LIKE '%s'
ORDER BY time ORDER BY time
''' % (where_sql, like_sql), jid_tuple) ''' % (where_sql, like_sql), jid_tuple)

View File

@ -56,6 +56,7 @@ class Column(IntEnum):
UNIXTIME = 2 UNIXTIME = 2
MESSAGE = 3 MESSAGE = 3
TIME = 4 TIME = 4
LOG_LINE_ID = 5
class HistoryWindow: class HistoryWindow:
""" """
@ -85,8 +86,8 @@ class HistoryWindow:
self.results_window = xml.get_object('results_scrolledwindow') self.results_window = xml.get_object('results_scrolledwindow')
self.search_in_date = xml.get_object('search_in_date') self.search_in_date = xml.get_object('search_in_date')
# contact_name, date, message, time # jid, contact_name, date, message, time, log_line_id
model = Gtk.ListStore(str, str, str, str, str) model = Gtk.ListStore(str, str, str, str, str, int)
self.results_treeview.set_model(model) self.results_treeview.set_model(model)
col = Gtk.TreeViewColumn(_('Name')) col = Gtk.TreeViewColumn(_('Name'))
self.results_treeview.append_column(col) self.results_treeview.append_column(col)
@ -393,28 +394,31 @@ class HistoryWindow:
show_status = self.show_status_checkbutton.get_active() show_status = self.show_status_checkbutton.get_active()
lines = gajim.logger.get_conversation_for_date(self.jid, year, month, day, self.account) lines = gajim.logger.get_conversation_for_date(self.jid, year, month, day, self.account)
# lines holds list with tupples that have:
# contact_name, time, kind, show, message
for line in lines: for line in lines:
# line[0] is contact_name, line[1] is time of message # line[0] is contact_name, line[1] is time of message
# line[2] is kind, line[3] is show, line[4] is message, line[5] is subject # line[2] is kind, line[3] is show, line[4] is message, line[5] is subject
# line[6] is additional_data # line[6] is additional_data, line[7] is log_line_id
if not show_status and line[2] in (KindConstant.GCSTATUS, if not show_status and line[2] in (KindConstant.GCSTATUS,
KindConstant.STATUS): KindConstant.STATUS):
continue continue
self._add_new_line(line[0], line[1], line[2], line[3], line[4], self._add_new_line(line[0], line[1], line[2], line[3], line[4],
line[5], line[6]) line[5], line[6], line[7])
def _add_new_line(self, contact_name, tim, kind, show, message, subject, additional_data): def _add_new_line(self, contact_name, tim, kind, show, message, subject,
additional_data, log_line_id):
""" """
Add a new line in textbuffer Add a new line in textbuffer
""" """
if not message and kind not in (KindConstant.STATUS, if not message and kind not in (KindConstant.STATUS,
KindConstant.GCSTATUS): KindConstant.GCSTATUS):
return return
buf = self.history_buffer buf = self.history_buffer
end_iter = buf.get_end_iter() end_iter = buf.get_end_iter()
# Make the beginning of every message searchable by its log_line_id
buf.create_mark(str(log_line_id), end_iter, left_gravity=True)
if gajim.config.get('print_time') == 'always': if gajim.config.get('print_time') == 'always':
timestamp_str = gajim.config.get('time_stamp') timestamp_str = gajim.config.get('time_stamp')
timestamp_str = helpers.from_one_line(timestamp_str) timestamp_str = helpers.from_one_line(timestamp_str)
@ -429,12 +433,6 @@ class HistoryWindow:
tim = time.strftime('%X ', time.localtime(float(tim))) tim = time.strftime('%X ', time.localtime(float(tim)))
buf.insert_with_tags_by_name(end_iter, tim + '\n', buf.insert_with_tags_by_name(end_iter, tim + '\n',
'time_sometimes') 'time_sometimes')
else: # don't print time. So we print it as invisible to be able to
# search for it
timestamp_str = gajim.config.get('time_stamp')
timestamp_str = helpers.from_one_line(timestamp_str)
tim = time.strftime(timestamp_str, time.localtime(float(tim)))
buf.insert_with_tags_by_name(end_iter, tim, 'invisible')
tag_name = '' tag_name = ''
tag_msg = '' tag_msg = ''
@ -561,12 +559,13 @@ class HistoryWindow:
contact_name = self.completion_dict[jid][InfoColumn.NAME] contact_name = self.completion_dict[jid][InfoColumn.NAME]
tim = row[1] tim = row[1]
message = row[4] message = row[4]
log_line_id = row[6]
local_time = time.localtime(tim) local_time = time.localtime(tim)
date = time.strftime('%Y-%m-%d', local_time) date = time.strftime('%Y-%m-%d', local_time)
# jid (to which log is assigned to), name, date, message, # jid (to which log is assigned to), name, date, message,
# time (full unix time) # time (full unix time)
model.append((jid, contact_name, date, message, str(tim))) model.append((jid, contact_name, date, message, str(tim), log_line_id))
def on_results_treeview_row_activated(self, widget, path, column): def on_results_treeview_row_activated(self, widget, path, column):
""" """
@ -594,28 +593,32 @@ class HistoryWindow:
self.calendar.select_month(month, year) self.calendar.select_month(month, year)
self.calendar.select_day(day) self.calendar.select_day(day)
unix_time = model[path][Column.TIME] self._scroll_to_message_and_highlight(model[path][Column.LOG_LINE_ID])
self._scroll_to_result(unix_time)
def _scroll_to_result(self, unix_time): def _scroll_to_message_and_highlight(self, log_line_id):
""" """
Scroll to the result using unix_time and highlight message Scroll to a message and highlight it
""" """
start_iter = self.history_buffer.get_start_iter()
local_time = time.localtime(float(unix_time)) def iterator_has_mark(iterator, mark_name):
timestamp_str = gajim.config.get('time_stamp') for mark in iterator.get_marks():
timestamp_str = helpers.from_one_line(timestamp_str) if mark.get_name() == mark_name:
tim = time.strftime(timestamp_str, local_time) return True
result = start_iter.forward_search(tim, Gtk.TextSearchFlags.TEXT_ONLY, return False
None)
if result is not None: log_line_id = str(log_line_id)
match_start_iter, match_end_iter = result line = self.history_buffer.get_start_iter()
match_end_iter.forward_to_tag_toggle(self.history_buffer.eol_tag) while not iterator_has_mark(line, log_line_id):
self.history_buffer.apply_tag_by_name('highlight', match_start_iter, if not line.forward_line():
match_end_iter) return
mark = self.history_buffer.create_mark('match', match_start_iter, True)
GLib.idle_add(self.history_textview.tv.scroll_to_mark, mark, match_start = line
0, True, 0.0, 0.5) 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)
def on_log_history_checkbutton_toggled(self, widget): def on_log_history_checkbutton_toggled(self, widget):
# log conversation history? # log conversation history?