Daniel Brötzmann pushed to branch mainwindow at gajim / gajim
Commits: 7591ff17 by wurstsalat at 2021-04-17T11:26:35+02:00 QuoteWidget: Add styling
- - - - - 8c631b02 by wurstsalat at 2021-04-17T23:48:13+02:00 PlainWidget: Strip text
- - - - - 58b4764d by wurstsalat at 2021-04-18T00:35:20+02:00 Add SearchView
- - - - -
11 changed files:
- gajim/chat_control_base.py - gajim/common/storage/archive.py - gajim/data/gui/main.ui - + gajim/data/gui/search_view.ui - gajim/data/style/gajim.css - gajim/gtk/conversation/plain_widget.py - gajim/gtk/conversation/quote_widget.py - gajim/gtk/conversation/view.py - gajim/gtk/main.py - + gajim/gtk/search_view.py - gajim/gui_menu_builder.py
Changes:
===================================== gajim/chat_control_base.py ===================================== @@ -174,6 +174,7 @@ def __init__(self, parent_win, widget_name, jid, acct,
self._scrolled_view = ScrolledView() self._scrolled_view.add(self.conversation_view) + self._scrolled_view.set_focus_vadjustment(Gtk.Adjustment()) self.xml.textview_box.add(self._scrolled_view) self.xml.textview_box.reorder_child(self._scrolled_view, 2) self._scrolled_view.connect('request-history', @@ -1144,6 +1145,7 @@ def add_message(self, display_marking=displaymarking, message_id=message_id, correct_id=correct_id, + log_line_id=msg_log_id, additional_data=additional_data, marker=marker, error=error) @@ -1391,6 +1393,25 @@ def set_control_active(self, state): def scroll_to_end(self, force=False): self.conversation_view.scroll_to_end(force)
+ def scroll_to_message(self, log_line_id, timestamp): + row = self.conversation_view.get_row_by_log_line_id(log_line_id) + if row is None: + first_row = self.conversation_view.get_first_message_row() + if first_row is None: + first_timestamp = time.time() + else: + first_timestamp = first_row.db_timestamp + messages = app.storage.archive.get_conversation_between( + self.account, self.contact.jid, first_timestamp, timestamp) + if not messages: + return + + self.add_messages(messages) + + GLib.idle_add( + self.conversation_view.scroll_to_message_and_highlight, + log_line_id) + def fetch_n_lines_history(self, _scrolled, n_lines): row = self.conversation_view.get_first_message_row() if row is None: @@ -1415,6 +1436,9 @@ def fetch_n_lines_history(self, _scrolled, n_lines): self._scrolled_view.set_history_complete(True) return
+ self.add_messages(messages) + + def add_messages(self, messages): for msg in messages: if not msg: continue @@ -1440,6 +1464,7 @@ def fetch_n_lines_history(self, _scrolled, n_lines): subject=msg.subject, additional_data=msg.additional_data, message_id=msg.message_id, + log_line_id=msg.log_line_id, marker=msg.marker, error=msg.error, history=True)
===================================== gajim/common/storage/archive.py ===================================== @@ -208,6 +208,9 @@ def _get_jid_ids_from_db(self): self._jid_ids[row.jid] = row self._jid_ids_reversed[row.jid_id] = row
+ def get_jid_from_id(self, jid_id): + return self._jid_ids_reversed[jid_id] + def get_jids_in_db(self): return self._jid_ids.keys()
@@ -418,6 +421,40 @@ def get_last_conversation_line(self, account, jid):
return self._con.execute(sql, tuple(jids)).fetchone()
+ @timeit + def get_conversation_between(self, account, jid, before, after): + """ + Load all lines of conversation with jid between two timestamps + + :param account: The account + + :param jid: The jid for which we request the conversation + + :param before: latest timestamp + + :param after: earliest timestamp + + returns a list of namedtuples + """ + jids = self._get_family_jids(account, jid) + account_id = self.get_account_id(account) + + sql = ''' + SELECT contact_name, time, kind, show, message, subject, + additional_data, log_line_id, message_id, + error as "error [common_error]", + marker as "marker [marker]" + FROM logs NATURAL JOIN jids WHERE jid IN ({jids}) + AND account_id = {account_id} + AND time < ? AND time >= ? + ORDER BY time DESC, log_line_id DESC + '''.format(jids=', '.join('?' * len(jids)), + account_id=account_id) + + return self._con.execute( + sql, + tuple(jids) + (before, after)).fetchall() + @timeit def get_messages_for_date(self, account, jid, date): """ @@ -477,6 +514,9 @@ def search_log(self, account, jid, query, date=None): """ jids = self._get_family_jids(account, jid)
+ kinds = map(str, [KindConstant.STATUS, + KindConstant.GCSTATUS]) + if date: delta = datetime.timedelta( hours=23, minutes=59, seconds=59, microseconds=999999) @@ -491,12 +531,36 @@ def search_log(self, account, jid, query, date=None): additional_data, log_line_id FROM logs NATURAL JOIN jids WHERE jid IN ({jids}) AND message LIKE like(?) {date_search} + AND kind NOT IN ({kinds}) ORDER BY time DESC, log_line_id '''.format(jids=', '.join('?' * len(jids)), - date_search=between if date else '') + date_search=between if date else '', + kinds=', '.join(kinds))
return self._con.execute(sql, tuple(jids) + (query,)).fetchall()
+ @timeit + def search_all_logs(self, query): + """ + Search all conversation logs for messages containing the `query` + string. + + :param query: A search string + + returns a list of namedtuples + """ + kinds = map(str, [KindConstant.STATUS, + KindConstant.GCSTATUS]) + sql = ''' + SELECT account_id, jid_id, contact_name, time, kind, show, message, + subject, additional_data, log_line_id + FROM logs WHERE message LIKE like(?) + AND kind NOT IN ({kinds}) + ORDER BY time DESC, log_line_id + '''.format(kinds=', '.join(kinds)) + + return self._con.execute(sql, (query,)).fetchall() + @timeit def get_days_with_logs(self, account, jid, year, month): """
===================================== gajim/data/gui/main.ui ===================================== @@ -141,7 +141,7 @@ <property name="position">250</property> <property name="position-set">True</property> <child> - <!-- n-columns=3 n-rows=3 --> + <!-- n-columns=1 n-rows=2 --> <object class="GtkGrid" id="middle_grid"> <property name="visible">True</property> <property name="can-focus">False</property> @@ -287,27 +287,6 @@ <property name="top-attach">1</property> </packing> </child> - <child> - <placeholder/> - </child> - <child> - <placeholder/> - </child> - <child> - <placeholder/> - </child> - <child> - <placeholder/> - </child> - <child> - <placeholder/> - </child> - <child> - <placeholder/> - </child> - <child> - <placeholder/> - </child> <style> <class name="middle-grid"/> </style> @@ -318,38 +297,26 @@ </packing> </child> <child> - <!-- n-columns=3 n-rows=3 --> + <!-- n-columns=1 n-rows=1 --> <object class="GtkGrid" id="right_grid"> <property name="visible">True</property> <property name="can-focus">False</property> <property name="hexpand">True</property> <property name="vexpand">True</property> <child> - <placeholder/> - </child> - <child> - <placeholder/> - </child> - <child> - <placeholder/> - </child> - <child> - <placeholder/> - </child> - <child> - <placeholder/> - </child> - <child> - <placeholder/> - </child> - <child> - <placeholder/> - </child> - <child> - <placeholder/> - </child> - <child> - <placeholder/> + <object class="GtkOverlay" id="right_grid_overlay"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="left-attach">0</property> + <property name="top-attach">0</property> + </packing> </child> </object> <packing>
===================================== gajim/data/gui/search_view.ui ===================================== @@ -0,0 +1,289 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.38.2 --> +<interface> + <requires lib="gtk+" version="3.24"/> + <object class="GtkBox" id="header_box"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="hexpand">True</property> + <property name="spacing">6</property> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="valign">center</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="header_name_label"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="ellipsize">end</property> + <property name="max-width-chars">15</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="header_date_label"> + <property name="visible">True</property> + <property name="can-focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="valign">center</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + <style> + <class name="search-view-header"/> + </style> + </object> + <!-- n-columns=3 n-rows=2 --> + <object class="GtkGrid" id="result_row_grid"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="row-spacing">3</property> + <property name="column-spacing">12</property> + <child> + <object class="GtkImage" id="row_avatar"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="valign">start</property> + </object> + <packing> + <property name="left-attach">0</property> + <property name="top-attach">0</property> + <property name="height">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="row_time_label"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">end</property> + <property name="single-line-mode">True</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left-attach">2</property> + <property name="top-attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="row_name_label"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="ellipsize">end</property> + <property name="single-line-mode">True</property> + <style> + <class name="bold"/> + </style> + </object> + <packing> + <property name="left-attach">1</property> + <property name="top-attach">0</property> + </packing> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <style> + <class name="search-view-row-grid"/> + </style> + </object> + <object class="GtkBox" id="search_box"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="hexpand">True</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="spacing">12</property> + <child type="center"> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="label" translatable="yes">Search</property> + <style> + <class name="bold16"/> + </style> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkButton"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">True</property> + <property name="tooltip-text" translatable="yes">Close Search</property> + <property name="relief">none</property> + <signal name="clicked" handler="_on_hide_clicked" swapped="no"/> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="icon-name">window-close-symbolic</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSearchEntry" id="search_entry"> + <property name="width-request">200</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="halign">center</property> + <property name="primary-icon-name">edit-find-symbolic</property> + <property name="primary-icon-activatable">False</property> + <property name="primary-icon-sensitive">False</property> + <signal name="activate" handler="_on_search" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="search_checkbutton"> + <property name="label" translatable="yes">Search everywhere</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="halign">center</property> + <property name="active">True</property> + <property name="draw-indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + <property name="can-focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <child> + <object class="GtkViewport"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <child> + <object class="GtkListBox" id="results_listbox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="selection-mode">none</property> + <signal name="row-activated" handler="_on_row_activated" swapped="no"/> + <child type="placeholder"> + <object class="GtkBox" id="placeholder"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="valign">center</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="icon-name">system-search-symbolic</property> + <property name="icon_size">6</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="label" translatable="yes">No results</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">4</property> + </packing> + </child> + <style> + <class name="search-view"/> + </style> + </object> +</interface>
===================================== gajim/data/style/gajim.css ===================================== @@ -59,8 +59,15 @@ .conversation-mention-highlight { background-color: rgb(255, 215, 194); } +@keyframes highlight { + from { background: rgb(194, 215, 255) } + to { background: transparent } +} .conversation-search-highlight { - background-color: rgb(194, 215, 255); + animation-duration: 3s; + animation-timing-function: ease-out; + animation-iteration-count: 1; + animation-name: highlight; } .conversation-system-row { padding: 18px; @@ -303,6 +310,33 @@ padding: 0px 18px 6px 18px; }
+/* Seach view */ +.search-view { + padding: 12px; + border-left: 1px solid @borders; + background-color: @theme_base_color; +} +.search-view-row-grid textview { + background: transparent; +} +.search-view-row-grid textview text { + background: transparent; +} +.search-view-row-grid label { + font-size: small; +} +.search-view-counter label { + font-style: italic; + color: @insensitive_fg_color; +} +.search-view-header { + padding: 12px 6px 6px 6px; +} +.search-view-header label { + font-size: small; + color: @insensitive_fg_color; +} + /* Profile window */ #NicknameEntry:disabled { font-size: 28px; @@ -711,7 +745,15 @@ popover.combo scrollbar.vertical {
messagedialog.confirmation-dialog > box { margin-top: 18px; }
- +.conversation-quote { + padding-bottom: 6px; +} +.conversation-quote textview text { + color: alpha(@theme_text_color, 0.6); +} +.conversation-quote-bar { + background-color: @unfocused_borders; +} .conversation-more-button { padding-top: 0px; padding-bottom:0px;
===================================== gajim/gtk/conversation/plain_widget.py ===================================== @@ -125,7 +125,7 @@ def get_text(self):
def print_text_with_styling(self, block): buffer_ = self.get_buffer() - buffer_.insert(buffer_.get_start_iter(), block.text) + buffer_.insert(buffer_.get_start_iter(), block.text.strip())
for span in block.spans: start_iter = buffer_.get_iter_at_offset(span.start)
===================================== gajim/gtk/conversation/quote_widget.py ===================================== @@ -18,8 +18,14 @@
class QuoteWidget(Gtk.Box): def __init__(self, account): - Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL) + Gtk.Box.__init__(self) self.set_vexpand(True) + self.get_style_context().add_class('conversation-quote') + quote_bar = Gtk.Box() + quote_bar.set_size_request(3, -1) + quote_bar.set_margin_right(6) + quote_bar.get_style_context().add_class('conversation-quote-bar') + self.add(quote_bar)
self._account = account
===================================== gajim/gtk/conversation/view.py ===================================== @@ -316,6 +316,20 @@ def reduce_message_count(self):
return successful
+ def scroll_to_message_and_highlight(self, log_line_id): + highlight_row = None + for row in self.get_children(): + row.get_style_context().remove_class( + 'conversation-search-highlight') + if row.log_line_id == log_line_id: + highlight_row = row + + if highlight_row is not None: + highlight_row.get_style_context().add_class( + 'conversation-search-highlight') + # This scrolls the ListBox to the highlighted row + highlight_row.grab_focus() + def _get_row_by_message_id(self, id_): return self._message_id_row_map.get(id_)
===================================== gajim/gtk/main.py ===================================== @@ -13,6 +13,7 @@
from gajim.gui.account_page import AccountPage from gajim.gui.adhoc import AdHocCommand +from gajim.gui.search_view import SearchView from gajim.gui.chat_list_stack import ChatListStack from gajim.gui.chat_stack import ChatStack from gajim.gui.account_side_bar import AccountSideBar @@ -57,9 +58,20 @@ def __init__(self): self._ui.app_image.set_from_surface(surface)
self._chat_stack = ChatStack() - self._ui.right_grid.add(self._chat_stack) + self._ui.right_grid_overlay.add(self._chat_stack) + + self._search_view = SearchView() + self._search_view.connect('hide-search', self._on_search_hide) + + self._search_revealer = Gtk.Revealer() + self._search_revealer.set_reveal_child(True) + self._search_revealer.set_halign(Gtk.Align.END) + self._search_revealer.set_no_show_all(True) + self._search_revealer.add(self._search_view) + self._ui.right_grid_overlay.add_overlay(self._search_revealer)
self._chat_list_stack = ChatListStack(self, self._ui, self._chat_stack) + self._chat_list_stack.connect('chat-selected', self._on_chat_selected) self._ui.chat_list_scrolled.add(self._chat_list_stack)
self._workspace_side_bar = WorkspaceSideBar(self._chat_list_stack) @@ -138,6 +150,7 @@ def _add_actions(self): ('toggle-chat-pinned', 'as', self._toggle_chat_pinned), ('move-chat-to-workspace', 'as', self._move_chat_to_workspace), ('add-to-roster', 'as', self._add_to_roster), + ('search-history', None, self._on_search_history), ]
for action in actions: @@ -207,6 +220,10 @@ def _on_action(self, action, _param): if res != Gdk.EVENT_PROPAGATE: return res
+ if action == 'escape': + if self._search_revealer.get_reveal_child(): + self._search_revealer.hide() + # if action == 'escape' and app.settings.get('escape_key_closes'): # self.remove_tab(control, self.CLOSE_ESC) # return @@ -559,6 +576,22 @@ def _load_chats(self): def _on_start_chat_clicked(_button): app.app.activate_action('start-chat', GLib.Variant('s', ''))
+ def _on_chat_selected(self, _chat_list_stack, _workspace_id, *args): + control = self.get_active_control() + if control is not None: + self._search_view.set_context(control.account, control.contact.jid) + + def _on_search_history(self, _action, _param): + control = self.get_active_control() + if control is not None: + self._search_view.set_context(control.account, control.contact.jid) + self._search_view.clear() + self._search_revealer.show() + self._search_view.set_focus() + + def _on_search_hide(self, *args): + self._search_revealer.hide() + def _on_event(self, event): if event.name == 'caps-update': #TODO
===================================== gajim/gtk/search_view.py ===================================== @@ -0,0 +1,253 @@ +# This file is part of Gajim. +# +# Gajim is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published +# by the Free Software Foundation; version 3 only. +# +# Gajim is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Gajim. If not, see http://www.gnu.org/licenses/. + +import logging +import time + +from gi.repository import Gdk +from gi.repository import GObject +from gi.repository import Gtk + +from gajim.common import app +from gajim.common.const import AvatarSize +from gajim.common.const import KindConstant +from gajim.common.i18n import _ +from gajim.common.styling import process + +from .conversation.message_widget import MessageWidget +from .util import get_builder + +log = logging.getLogger('gajim.gui.search_view') + + +class SearchView(Gtk.Box): + + __gsignals__ = { + 'hide-search': ( + GObject.SignalFlags.RUN_FIRST, + None, + ()), + } + + def __init__(self): + Gtk.Box.__init__(self) + self.set_size_request(300, -1) + + self._account = None + self._jid = None + + self._ui = get_builder('search_view.ui') + self._ui.results_listbox.set_header_func(self._header_func) + self.add(self._ui.search_box) + + self._ui.connect_signals(self) + self.connect('key-press-event', self._on_key_press) + self.show_all() + + def _on_key_press(self, _widget, event): + if event.keyval == Gdk.KEY_Escape: + self.emit('hide-search') + + @staticmethod + def _header_func(row, before): + if before is None: + if row.type == 'counter': + row.set_header(None) + else: + row.set_header(RowHeader(row.account, row.jid, row.time)) + else: + date1 = time.strftime('%x', time.localtime(row.time)) + date2 = time.strftime('%x', time.localtime(before.time)) + if before.jid != row.jid: + row.set_header(RowHeader(row.account, row.jid, row.time)) + elif date1 != date2: + row.set_header(RowHeader(row.account, row.jid, row.time)) + else: + row.set_header(None) + + def _on_hide_clicked(self, _button): + self.emit('hide-search') + + def clear(self): + self._ui.search_entry.set_text('') + self._clear_results() + + def _clear_results(self): + for row in self._ui.results_listbox.get_children(): + self._ui.results_listbox.remove(row) + + def _on_search(self, entry): + self._clear_results() + text = entry.get_text() + if text == '': + return + + accounts = self._get_accounts() + everywhere = self._ui.search_checkbutton.get_active() + context = self._account is not None and self._jid is not None + + if not context or everywhere: + # Global search + results = app.storage.archive.search_all_logs(text) + results_count = len(results) + if results_count: + self._ui.results_listbox.add(CounterRow(results_count)) + for msg in results: + result_row = ResultRow( + msg, + accounts.get(msg.account_id), + app.storage.archive.get_jid_from_id(msg.jid_id).jid) + self._ui.results_listbox.add(result_row) + else: + results = app.storage.archive.search_log( + self._account, self._jid, text) + results_count = len(results) + if results_count: + self._ui.results_listbox.add(CounterRow(results_count)) + for msg in results: + result_row = ResultRow(msg, self._account, self._jid) + self._ui.results_listbox.add(result_row) + + @staticmethod + def _get_accounts(): + accounts = {} + for account in app.settings.get_accounts(): + account_id = app.storage.archive.get_account_id(account) + accounts[account_id] = account + return accounts + + @staticmethod + def _on_row_activated(_listbox, row): + if row.type == 'counter': + return + + control = app.window.get_active_control() + if control is not None: + if control.contact.jid == row.jid: + control.scroll_to_message(row.log_line_id, row.timestamp) + return + + # Wrong chat or no control opened + # TODO: type 'pm' is KindConstant.CHAT_MSG_RECV, too + app.window.add_chat(row.account, row.jid, row.type, select=True) + control = app.window.get_active_control() + control.scroll_to_message(row.log_line_id, row.timestamp) + + def set_focus(self): + self._ui.search_entry.grab_focus() + + def set_context(self, account, jid): + self._account = account + self._jid = jid + self._ui.search_checkbutton.set_active(False) + + +class RowHeader(Gtk.Box): + def __init__(self, account, jid, timestamp): + Gtk.Box.__init__(self) + self.set_hexpand(True) + + self._ui = get_builder('search_view.ui') + self.add(self._ui.header_box) + + client = app.get_client(account) + contact = client.get_module('Contacts').get_contact(jid) + self._ui.header_name_label.set_text(contact.name or '') + + local_time = time.localtime(timestamp) + date = time.strftime('%x', local_time) + self._ui.header_date_label.set_text(date) + + self.show_all() + + +class CounterRow(Gtk.ListBoxRow): + def __init__(self, count): + Gtk.ListBoxRow.__init__(self) + self.type = 'counter' + self.jid = '' + self.time = 0 + self.get_style_context().add_class('search-view-counter') + + if count == 1: + counter_text = _('1 result') + else: + counter_text = _('%s results') % count + label = Gtk.Label(label=counter_text) + self.add(label) + self.show_all() + + +class ResultRow(Gtk.ListBoxRow): + def __init__(self, msg, account, jid): + Gtk.ListBoxRow.__init__(self) + self.account = account + self.jid = jid + self.time = msg.time + self._client = app.get_client(account) + + self.contact = self._client.get_module('Contacts').get_contact(jid) + + self.log_line_id = msg.log_line_id + self.timestamp = msg.time + self.kind = msg.kind + + self.type = 'contact' + if msg.kind == KindConstant.GC_MSG: + self.type = 'groupchat' + + self._ui = get_builder('search_view.ui') + self.add(self._ui.result_row_grid) + + kind = 'status' + contact_name = msg.contact_name + if msg.kind in ( + KindConstant.SINGLE_MSG_RECV, KindConstant.CHAT_MSG_RECV): + kind = 'incoming' + contact_name = self.contact.name + elif msg.kind == KindConstant.GC_MSG: + kind = 'incoming' + elif msg.kind in ( + KindConstant.SINGLE_MSG_SENT, KindConstant.CHAT_MSG_SENT): + kind = 'outgoing' + contact_name = app.nicks[account] + self._ui.row_name_label.set_text(contact_name) + + avatar = self._get_avatar(kind, contact_name) + self._ui.row_avatar.set_from_surface(avatar) + + local_time = time.localtime(msg.time) + date = time.strftime('%H:%M', local_time) + self._ui.row_time_label.set_label(date) + + message_widget = MessageWidget(account) + self._ui.result_row_grid.attach(message_widget, 1, 1, 2, 1) + result = process(msg.message) + message_widget.add_content(result.blocks) + + self.show_all() + + def _get_avatar(self, kind, name): + scale = self.get_scale_factor() + if self.contact.is_groupchat: + contact = self.contact.get_resource(name) + return contact.get_avatar(AvatarSize.ROSTER, scale, add_show=False) + + if kind == 'outgoing': + contact = self._client.get_module('Contacts').get_contact( + str(self._client.get_own_jid().bare)) + else: + contact = self.contact + + return contact.get_avatar(AvatarSize.ROSTER, scale, add_show=False)
===================================== gajim/gui_menu_builder.py ===================================== @@ -525,6 +525,7 @@ def get_singlechat_menu(control_id, account, jid, type_): ('win.start-call-', _('Start Call…')), ('win.information-', _('Information')), ('app.browse-history', _('History')), + ('win.search-history', _('Search…')), ]
def build_chatstate_menu(): @@ -549,7 +550,11 @@ def build_menu(preset): if action_name == 'win.send-marker-' and type_ == 'pm': continue
- if action_name == 'app.browse-history': + if action_name == 'win.search-history': + menuitem = Gio.MenuItem.new(label, action_name) + menuitem.set_action_and_target_value(action_name, None) + menu.append_item(menuitem) + elif action_name == 'app.browse-history': menuitem = Gio.MenuItem.new(label, action_name) dict_ = {'account': GLib.Variant('s', account), 'jid': GLib.Variant('s', str(jid))} @@ -586,6 +591,7 @@ def get_groupchat_menu(control_id, account, jid): ('win.request-voice-', _('Request Voice')), ('win.execute-command-', _('Execute Command…')), ('app.browse-history', _('History')), + ('win.search-history', _('Search…')), ]
def build_menu(preset): @@ -593,7 +599,12 @@ def build_menu(preset): for item in preset: if isinstance(item[1], str): action_name, label = item - if action_name == 'app.browse-history': + if action_name == 'win.search-history': + menuitem = Gio.MenuItem.new(label, action_name) + menuitem.set_action_and_target_value(action_name, None) + menu.append_item(menuitem) + + elif action_name == 'app.browse-history': menuitem = Gio.MenuItem.new(label, action_name) dict_ = {'account': GLib.Variant('s', account), 'jid': GLib.Variant('s', jid)}
View it on GitLab: https://dev.gajim.org/gajim/gajim/-/compare/d07283dab924d9c300f94d44dba671c9...