2018-07-25 18:41:51 +00:00
|
|
|
using Gee;
|
2018-07-09 22:31:39 +00:00
|
|
|
using Gtk;
|
|
|
|
using Pango;
|
|
|
|
|
|
|
|
using Dino.Entities;
|
|
|
|
|
|
|
|
namespace Dino.Ui {
|
|
|
|
|
|
|
|
[GtkTemplate (ui = "/im/dino/Dino/global_search.ui")]
|
2018-08-01 13:20:56 +00:00
|
|
|
class GlobalSearch : Overlay {
|
2018-07-25 18:41:51 +00:00
|
|
|
public signal void selected_item(MessageItem item);
|
2018-07-09 22:31:39 +00:00
|
|
|
private StreamInteractor stream_interactor;
|
|
|
|
private string search = "";
|
|
|
|
private int loaded_results = -1;
|
|
|
|
private Mutex reloading_mutex = Mutex();
|
|
|
|
|
|
|
|
[GtkChild] public SearchEntry search_entry;
|
|
|
|
[GtkChild] public Label entry_number_label;
|
|
|
|
[GtkChild] public ScrolledWindow results_scrolled;
|
|
|
|
[GtkChild] public Box results_box;
|
2018-07-25 18:41:51 +00:00
|
|
|
[GtkChild] public Stack results_empty_stack;
|
2018-08-01 13:20:56 +00:00
|
|
|
[GtkChild] public Frame auto_complete_overlay;
|
|
|
|
[GtkChild] public ListBox auto_complete_list;
|
2018-07-09 22:31:39 +00:00
|
|
|
|
|
|
|
public GlobalSearch init(StreamInteractor stream_interactor) {
|
|
|
|
this.stream_interactor = stream_interactor;
|
|
|
|
|
|
|
|
search_entry.search_changed.connect(() => {
|
|
|
|
set_search(search_entry.text);
|
|
|
|
});
|
2018-08-01 13:20:56 +00:00
|
|
|
search_entry.notify["text"].connect_after(() => { update_auto_complete(); });
|
|
|
|
search_entry.notify["cursor-position"].connect_after(() => { update_auto_complete(); });
|
2018-07-09 22:31:39 +00:00
|
|
|
|
|
|
|
results_scrolled.vadjustment.notify["value"].connect(() => {
|
|
|
|
if (results_scrolled.vadjustment.upper - (results_scrolled.vadjustment.value + results_scrolled.vadjustment.page_size) < 100) {
|
|
|
|
if (!reloading_mutex.trylock()) return;
|
2018-07-25 18:41:51 +00:00
|
|
|
Gee.List<MessageItem> new_messages = stream_interactor.get_module(SearchProcessor.IDENTITY).match_messages(search, loaded_results);
|
2018-07-09 22:31:39 +00:00
|
|
|
if (new_messages.size == 0) {
|
|
|
|
reloading_mutex.unlock();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
loaded_results += new_messages.size;
|
|
|
|
append_messages(new_messages);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
results_scrolled.vadjustment.notify["upper"].connect_after(() => {
|
|
|
|
reloading_mutex.trylock();
|
|
|
|
reloading_mutex.unlock();
|
|
|
|
});
|
2018-08-01 13:20:56 +00:00
|
|
|
|
|
|
|
event.connect((event) => {
|
|
|
|
if (auto_complete_overlay.visible) {
|
|
|
|
if (event.type == Gdk.EventType.KEY_PRESS && event.key.keyval == Gdk.Key.Up) {
|
|
|
|
var row = auto_complete_list.get_selected_row();
|
|
|
|
var index = row == null ? -1 : row.get_index() - 1;
|
|
|
|
if (index == -1) index = (int)auto_complete_list.get_children().length() - 1;
|
|
|
|
auto_complete_list.select_row(auto_complete_list.get_row_at_index(index));
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (event.type == Gdk.EventType.KEY_PRESS && event.key.keyval == Gdk.Key.Down) {
|
|
|
|
var row = auto_complete_list.get_selected_row();
|
|
|
|
var index = row == null ? 0 : row.get_index() + 1;
|
|
|
|
if (index == auto_complete_list.get_children().length()) index = 0;
|
|
|
|
auto_complete_list.select_row(auto_complete_list.get_row_at_index(index));
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (event.type == Gdk.EventType.KEY_PRESS && event.key.keyval == Gdk.Key.Tab ||
|
|
|
|
event.type == Gdk.EventType.KEY_RELEASE && event.key.keyval == Gdk.Key.Return) {
|
|
|
|
auto_complete_list.get_selected_row().activate();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// TODO: Handle cursor movement in results
|
|
|
|
// TODO: Direct all keystrokes to text input
|
|
|
|
return false;
|
|
|
|
});
|
|
|
|
|
2018-07-09 22:31:39 +00:00
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
2018-08-01 13:20:56 +00:00
|
|
|
private void update_auto_complete() {
|
|
|
|
Gee.List<SearchSuggestion> suggestions = stream_interactor.get_module(SearchProcessor.IDENTITY).suggest_auto_complete(search_entry.text, search_entry.cursor_position);
|
|
|
|
auto_complete_overlay.visible = suggestions.size > 0;
|
|
|
|
if (suggestions.size > 0) {
|
|
|
|
auto_complete_list.@foreach((widget) => auto_complete_list.remove(widget));
|
|
|
|
foreach(SearchSuggestion suggestion in suggestions) {
|
|
|
|
Builder builder = new Builder.from_resource("/im/dino/Dino/search_autocomplete.ui");
|
|
|
|
AvatarImage avatar = (AvatarImage)builder.get_object("image");
|
|
|
|
avatar.set_jid(stream_interactor, suggestion.jid, suggestion.account);
|
|
|
|
Label label = (Label)builder.get_object("label");
|
|
|
|
string display_name = Util.get_display_name(stream_interactor, suggestion.jid, suggestion.account);
|
|
|
|
if (display_name != suggestion.jid.to_string()) {
|
|
|
|
label.set_markup(@"$display_name <span font_weight='light' fgalpha='80%'>$(suggestion.jid)</span>");
|
|
|
|
} else {
|
|
|
|
label.label = display_name;
|
|
|
|
}
|
|
|
|
ListBoxRow row = new ListBoxRow() { visible = true, can_focus = false };
|
|
|
|
row.add((Widget)builder.get_object("root"));
|
|
|
|
row.activate.connect(() => {
|
|
|
|
handle_suggestion(suggestion);
|
|
|
|
});
|
|
|
|
auto_complete_list.add(row);
|
|
|
|
}
|
|
|
|
auto_complete_list.select_row(auto_complete_list.get_row_at_index(0));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private void handle_suggestion(SearchSuggestion suggestion) {
|
|
|
|
search_entry.move_cursor(MovementStep.LOGICAL_POSITIONS, suggestion.start_index - search_entry.cursor_position, false);
|
|
|
|
search_entry.delete_from_cursor(DeleteType.CHARS, suggestion.end_index - suggestion.start_index);
|
|
|
|
search_entry.insert_at_cursor(suggestion.completion + " ");
|
|
|
|
}
|
|
|
|
|
2018-07-09 22:31:39 +00:00
|
|
|
private void clear_search() {
|
|
|
|
results_box.@foreach((widget) => { widget.destroy(); });
|
|
|
|
}
|
|
|
|
|
|
|
|
private void set_search(string search) {
|
|
|
|
clear_search();
|
|
|
|
this.search = search;
|
|
|
|
|
2018-07-25 18:41:51 +00:00
|
|
|
if (get_keywords(search).is_empty) {
|
|
|
|
results_empty_stack.set_visible_child_name("empty");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
Gee.List<MessageItem> messages = stream_interactor.get_module(SearchProcessor.IDENTITY).match_messages(search);
|
|
|
|
if (messages.size == 0) {
|
|
|
|
results_empty_stack.set_visible_child_name("no-result");
|
|
|
|
} else {
|
|
|
|
results_empty_stack.set_visible_child_name("results");
|
|
|
|
|
|
|
|
int match_count = messages.size < 10 ? messages.size : stream_interactor.get_module(SearchProcessor.IDENTITY).count_match_messages(search);
|
|
|
|
entry_number_label.label = "<i>" + _("%i search results").printf(match_count) + "</i>";
|
|
|
|
loaded_results += messages.size;
|
|
|
|
append_messages(messages);
|
|
|
|
}
|
2018-07-09 22:31:39 +00:00
|
|
|
}
|
|
|
|
|
2018-07-25 18:41:51 +00:00
|
|
|
private void append_messages(Gee.List<MessageItem> messages) {
|
|
|
|
foreach (MessageItem item in messages) {
|
|
|
|
Gee.List<MessageItem> before_message = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages_before_message(item.conversation, item.message.local_time, item.message.id, 1);
|
|
|
|
Gee.List<MessageItem> after_message = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages_after_message(item.conversation, item.message.local_time, item.message.id, 1);
|
2018-07-09 22:31:39 +00:00
|
|
|
|
|
|
|
Box context_box = new Box(Orientation.VERTICAL, 5) { visible=true };
|
|
|
|
if (before_message != null && before_message.size > 0) {
|
|
|
|
context_box.add(get_context_message_widget(before_message.first()));
|
|
|
|
}
|
2018-07-25 18:41:51 +00:00
|
|
|
|
|
|
|
Widget match_widget = get_match_message_widget(item);
|
|
|
|
Util.force_alloc_width(match_widget, results_empty_stack.get_allocated_width() - results_box.margin * 2);
|
|
|
|
context_box.add(match_widget);
|
|
|
|
|
2018-07-09 22:31:39 +00:00
|
|
|
if (after_message != null && after_message.size > 0) {
|
|
|
|
context_box.add(get_context_message_widget(after_message.first()));
|
|
|
|
}
|
|
|
|
|
2018-07-25 18:41:51 +00:00
|
|
|
Label date_label = new Label(ConversationSummary.DefaultSkeletonHeader.get_relative_time(item.display_time)) { xalign=0, visible=true };
|
2018-07-09 22:31:39 +00:00
|
|
|
date_label.get_style_context().add_class("dim-label");
|
|
|
|
|
2018-07-25 18:41:51 +00:00
|
|
|
string display_name = Util.get_conversation_display_name(stream_interactor, item.conversation);
|
|
|
|
string title = item.message.type_ == Message.Type.GROUPCHAT ? _("In %s").printf(display_name) : _("With %s").printf(display_name);
|
2018-07-09 22:31:39 +00:00
|
|
|
Box header_box = new Box(Orientation.HORIZONTAL, 10) { margin_left=7, visible=true };
|
|
|
|
header_box.add(new Label(@"<b>$(Markup.escape_text(title))</b>") { ellipsize=EllipsizeMode.END, xalign=0, use_markup=true, visible=true });
|
|
|
|
header_box.add(date_label);
|
|
|
|
|
|
|
|
Box result_box = new Box(Orientation.VERTICAL, 7) { visible=true };
|
|
|
|
result_box.add(header_box);
|
|
|
|
result_box.add(context_box);
|
|
|
|
|
|
|
|
results_box.add(result_box);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-07-25 18:41:51 +00:00
|
|
|
private Widget get_match_message_widget(MessageItem item) {
|
|
|
|
Grid grid = get_skeleton(item);
|
2018-07-09 22:31:39 +00:00
|
|
|
grid.margin_top = 3;
|
|
|
|
grid.margin_bottom = 3;
|
|
|
|
|
2018-07-25 18:41:51 +00:00
|
|
|
string text = item.message.body.replace("\n", "").replace("\r", "");
|
2018-07-09 22:31:39 +00:00
|
|
|
if (text.length > 200) {
|
|
|
|
int index = text.index_of(search);
|
|
|
|
if (index + search.length <= 100) {
|
|
|
|
text = text.substring(0, 150) + " … " + text.substring(text.length - 50, 50);
|
|
|
|
} else if (index >= text.length - 100) {
|
|
|
|
text = text.substring(0, 50) + " … " + text.substring(text.length - 150, 150);
|
|
|
|
} else {
|
|
|
|
text = text.substring(0, 25) + " … " + text.substring(index - 50, 50) + text.substring(index, 100) + " … " + text.substring(text.length - 25, 25);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
TextView tv = new TextView() { wrap_mode=Gtk.WrapMode.WORD_CHAR, hexpand=true, visible=true };
|
|
|
|
tv.buffer.text = text;
|
|
|
|
TextTag link_tag = tv.buffer.create_tag("hit", background: "yellow");
|
|
|
|
|
2018-07-25 18:41:51 +00:00
|
|
|
Gee.List<string> keywords = get_keywords(Regex.escape_string(search.down()));
|
|
|
|
foreach (string keyword in keywords) {
|
|
|
|
Regex url_regex = new Regex(keyword.down());
|
|
|
|
MatchInfo match_info;
|
|
|
|
url_regex.match(text.down(), 0, out match_info);
|
|
|
|
for (; match_info.matches(); match_info.next()) {
|
|
|
|
int start;
|
|
|
|
int end;
|
|
|
|
match_info.fetch_pos(0, out start, out end);
|
|
|
|
start = text[0:start].char_count();
|
|
|
|
end = text[0:end].char_count();
|
|
|
|
TextIter start_iter;
|
|
|
|
TextIter end_iter;
|
|
|
|
tv.buffer.get_iter_at_offset(out start_iter, start);
|
|
|
|
tv.buffer.get_iter_at_offset(out end_iter, end);
|
|
|
|
tv.buffer.apply_tag(link_tag, start_iter, end_iter);
|
|
|
|
}
|
2018-07-09 22:31:39 +00:00
|
|
|
}
|
|
|
|
|
2018-07-25 18:41:51 +00:00
|
|
|
grid.attach(tv, 1, 1, 1, 1);
|
2018-07-09 22:31:39 +00:00
|
|
|
|
|
|
|
Button button = new Button() { relief=ReliefStyle.NONE, visible=true };
|
2018-07-25 18:41:51 +00:00
|
|
|
button.clicked.connect(() => {
|
|
|
|
selected_item(item);
|
|
|
|
});
|
2018-07-09 22:31:39 +00:00
|
|
|
button.add(grid);
|
|
|
|
return button;
|
|
|
|
}
|
|
|
|
|
2018-07-25 18:41:51 +00:00
|
|
|
private Grid get_context_message_widget(MessageItem item) {
|
|
|
|
Grid grid = get_skeleton(item);
|
2018-07-09 22:31:39 +00:00
|
|
|
grid.margin_left = 7;
|
2018-07-25 18:41:51 +00:00
|
|
|
Label label = new Label(item.message.body.replace("\n", "").replace("\r", "")) { ellipsize=EllipsizeMode.MIDDLE, xalign=0, visible=true };
|
2018-07-09 22:31:39 +00:00
|
|
|
grid.attach(label, 1, 1, 1, 1);
|
|
|
|
grid.opacity = 0.55;
|
|
|
|
return grid;
|
|
|
|
}
|
|
|
|
|
2018-07-25 18:41:51 +00:00
|
|
|
private Grid get_skeleton(MessageItem item) {
|
2018-07-09 22:31:39 +00:00
|
|
|
AvatarImage image = new AvatarImage() { height=32, width=32, margin_right=7, valign=Align.START, visible=true, allow_gray = false };
|
2018-07-25 18:41:51 +00:00
|
|
|
image.set_jid(stream_interactor, item.jid, item.message.account);
|
2018-07-09 22:31:39 +00:00
|
|
|
Grid grid = new Grid() { row_homogeneous=false, visible=true };
|
|
|
|
grid.attach(image, 0, 0, 1, 2);
|
|
|
|
|
2018-07-25 18:41:51 +00:00
|
|
|
string display_name = Util.get_display_name(stream_interactor, item.jid, item.message.account);
|
|
|
|
string color = Util.get_name_hex_color(stream_interactor, item.message.account, item.jid, false); // TODO Util.is_dark_theme(name_label)
|
2018-07-09 22:31:39 +00:00
|
|
|
Label name_label = new Label("") { use_markup=true, xalign=0, visible=true };
|
|
|
|
name_label.label = @"<span size='small' foreground=\"#$color\">$display_name</span>";
|
|
|
|
grid.attach(name_label, 1, 0, 1, 1);
|
|
|
|
return grid;
|
|
|
|
}
|
2018-07-25 18:41:51 +00:00
|
|
|
|
|
|
|
private static Gee.List<string> get_keywords(string search_string) {
|
|
|
|
Gee.List<string> ret = new ArrayList<string>();
|
|
|
|
foreach (string search in search_string.split(" ")) {
|
|
|
|
bool is_filter = search.has_prefix("from:") || search.has_prefix("in:") || search.has_prefix("with:");
|
|
|
|
if (!is_filter && search != "") {
|
|
|
|
ret.add(search);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ret;
|
|
|
|
}
|
2018-07-09 22:31:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|