add suggestions/auto-complete for search filters
This commit is contained in:
parent
b0c94641fc
commit
c0844bdea4
|
@ -31,19 +31,19 @@ public class SearchProcessor : StreamInteractionModule, Object {
|
||||||
foreach(string word in query.split(" ")) {
|
foreach(string word in query.split(" ")) {
|
||||||
if (word.has_prefix("with:")) {
|
if (word.has_prefix("with:")) {
|
||||||
if (with == null) {
|
if (with == null) {
|
||||||
with = word.substring(5) + "%";
|
with = word.substring(5);
|
||||||
} else {
|
} else {
|
||||||
return db.message.select().where("0");
|
return db.message.select().where("0");
|
||||||
}
|
}
|
||||||
} else if (word.has_prefix("in:")) {
|
} else if (word.has_prefix("in:")) {
|
||||||
if (in_ == null) {
|
if (in_ == null) {
|
||||||
in_ = word.substring(3) + "%";
|
in_ = word.substring(3);
|
||||||
} else {
|
} else {
|
||||||
return db.message.select().where("0");
|
return db.message.select().where("0");
|
||||||
}
|
}
|
||||||
} else if (word.has_prefix("from:")) {
|
} else if (word.has_prefix("from:")) {
|
||||||
if (from == null) {
|
if (from == null) {
|
||||||
from = word.substring(5) + "%";
|
from = word.substring(5);
|
||||||
} else {
|
} else {
|
||||||
return db.message.select().where("0");
|
return db.message.select().where("0");
|
||||||
}
|
}
|
||||||
|
@ -90,9 +90,143 @@ public class SearchProcessor : StreamInteractionModule, Object {
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Gee.List<SearchSuggestion> suggest_auto_complete(string query, int cursor_position, int limit = 5) {
|
||||||
|
int after_prev_space = query.substring(0, cursor_position).last_index_of(" ") + 1;
|
||||||
|
int next_space = query.index_of(" ", after_prev_space);
|
||||||
|
if (next_space < 0) next_space = query.length;
|
||||||
|
string current_query = query.substring(after_prev_space, next_space - after_prev_space);
|
||||||
|
Gee.List<SearchSuggestion> suggestions = new ArrayList<SearchSuggestion>();
|
||||||
|
|
||||||
|
if (current_query.has_prefix("from:")) {
|
||||||
|
if (cursor_position < after_prev_space + 5) return suggestions;
|
||||||
|
string current_from = current_query.substring(5);
|
||||||
|
string[] splitted = query.split(" ");
|
||||||
|
foreach(string s in splitted) {
|
||||||
|
if (s.has_prefix("from:") && s != "from:" + current_from) {
|
||||||
|
// Already have an from: filter -> no useful autocompletion possible
|
||||||
|
return suggestions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
string? current_in = null;
|
||||||
|
string? current_with = null;
|
||||||
|
foreach(string s in splitted) {
|
||||||
|
if (s.has_prefix("in:")) {
|
||||||
|
current_in = s.substring(3);
|
||||||
|
} else if (s.has_prefix("with:")) {
|
||||||
|
current_with = s.substring(5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (current_in != null && current_with != null) {
|
||||||
|
// in: and with: -> no useful autocompletion possible
|
||||||
|
return suggestions;
|
||||||
|
}
|
||||||
|
if (current_with != null) {
|
||||||
|
// Can only be the other one or us
|
||||||
|
|
||||||
|
// Normal chat
|
||||||
|
QueryBuilder chats = db.conversation.select()
|
||||||
|
.join_with(db.jid, db.jid.id, db.conversation.jid_id)
|
||||||
|
.join_with(db.account, db.account.id, db.conversation.account_id)
|
||||||
|
.with(db.jid.bare_jid, "=", current_with)
|
||||||
|
.with(db.account.enabled, "=", true)
|
||||||
|
.with(db.conversation.type_, "=", Conversation.Type.CHAT)
|
||||||
|
.order_by(db.conversation.last_active, "DESC");
|
||||||
|
foreach(Row chat in chats) {
|
||||||
|
if (suggestions.size == 0) {
|
||||||
|
suggestions.add(new SearchSuggestion(new Account.from_row(db, chat), new Jid(chat[db.jid.bare_jid]), "from:"+chat[db.jid.bare_jid], after_prev_space, next_space));
|
||||||
|
}
|
||||||
|
suggestions.add(new SearchSuggestion(new Account.from_row(db, chat), new Jid(chat[db.account.bare_jid]), "from:"+chat[db.account.bare_jid], after_prev_space, next_space));
|
||||||
|
}
|
||||||
|
return suggestions;
|
||||||
|
}
|
||||||
|
if (current_in != null) {
|
||||||
|
// All members of the MUC with history
|
||||||
|
QueryBuilder msgs = db.message.select()
|
||||||
|
.select_string(@"account.*, $(db.message.counterpart_resource)")
|
||||||
|
.join_with(db.jid, db.jid.id, db.message.counterpart_id)
|
||||||
|
.join_with(db.account, db.account.id, db.message.account_id)
|
||||||
|
.with(db.jid.bare_jid, "=", current_in)
|
||||||
|
.with(db.account.enabled, "=", true)
|
||||||
|
.with(db.message.type_, "=", Message.Type.GROUPCHAT)
|
||||||
|
.with(db.message.counterpart_resource, "LIKE", @"%$current_from%")
|
||||||
|
.group_by({db.message.counterpart_resource})
|
||||||
|
.order_by_name(@"MAX($(db.message.time))", "DESC")
|
||||||
|
.limit(5);
|
||||||
|
foreach(Row msg in msgs) {
|
||||||
|
suggestions.add(new SearchSuggestion(new Account.from_row(db, msg), new Jid(current_in).with_resource(msg[db.message.counterpart_resource]), "from:"+msg[db.message.counterpart_resource], after_prev_space, next_space));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: auto complete from
|
||||||
|
} else if (current_query.has_prefix("with:")) {
|
||||||
|
if (cursor_position < after_prev_space + 5) return suggestions;
|
||||||
|
string current_with = current_query.substring(5);
|
||||||
|
string[] splitted = query.split(" ");
|
||||||
|
foreach(string s in splitted) {
|
||||||
|
if ((s.has_prefix("with:") && s != "with:" + current_with) || s.has_prefix("in:")) {
|
||||||
|
// Already have an in: or with: filter -> no useful autocompletion possible
|
||||||
|
return suggestions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal chat
|
||||||
|
QueryBuilder chats = db.conversation.select()
|
||||||
|
.join_with(db.jid, db.jid.id, db.conversation.jid_id)
|
||||||
|
.join_with(db.account, db.account.id, db.conversation.account_id)
|
||||||
|
.outer_join_on(db.roster, @"$(db.jid.bare_jid) = $(db.roster.jid) AND $(db.account.id) = $(db.roster.account_id)")
|
||||||
|
.where(@"$(db.jid.bare_jid) LIKE ? OR $(db.roster.handle) LIKE ?", {@"%$current_with%", @"%$current_with%"})
|
||||||
|
.with(db.account.enabled, "=", true)
|
||||||
|
.with(db.conversation.type_, "=", Conversation.Type.CHAT)
|
||||||
|
.order_by(db.conversation.last_active, "DESC")
|
||||||
|
.limit(limit);
|
||||||
|
foreach(Row chat in chats) {
|
||||||
|
suggestions.add(new SearchSuggestion(new Account.from_row(db, chat), new Jid(chat[db.jid.bare_jid]), "with:"+chat[db.jid.bare_jid], after_prev_space, next_space) { order = chat[db.conversation.last_active]});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Groupchat PM
|
||||||
|
if (suggestions.size < 5) {
|
||||||
|
chats = db.conversation.select()
|
||||||
|
.join_with(db.jid, db.jid.id, db.conversation.jid_id)
|
||||||
|
.join_with(db.account, db.account.id, db.conversation.account_id)
|
||||||
|
.where(@"$(db.jid.bare_jid) LIKE ? OR $(db.conversation.resource) LIKE ?", {@"%$current_with%", @"%$current_with%"})
|
||||||
|
.with(db.account.enabled, "=", true)
|
||||||
|
.with(db.conversation.type_, "=", Conversation.Type.GROUPCHAT_PM)
|
||||||
|
.order_by(db.conversation.last_active, "DESC")
|
||||||
|
.limit(limit - suggestions.size);
|
||||||
|
foreach(Row chat in chats) {
|
||||||
|
suggestions.add(new SearchSuggestion(new Account.from_row(db, chat), new Jid(chat[db.jid.bare_jid]).with_resource(chat[db.conversation.resource]), "with:"+chat[db.jid.bare_jid]+"/"+chat[db.conversation.resource], after_prev_space, next_space) { order = chat[db.conversation.last_active]});
|
||||||
|
}
|
||||||
|
suggestions.sort((a, b) => (int)(b.order - a.order));
|
||||||
|
}
|
||||||
|
} else if (current_query.has_prefix("in:")) {
|
||||||
|
if (cursor_position < after_prev_space + 3) return suggestions;
|
||||||
|
string current_in = current_query.substring(3);
|
||||||
|
string[] splitted = query.split(" ");
|
||||||
|
foreach(string s in splitted) {
|
||||||
|
if ((s.has_prefix("in:") && s != "in:" + current_in) || s.has_prefix("with:")) {
|
||||||
|
// Already have an in: or with: filter -> no useful autocompletion possible
|
||||||
|
return suggestions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QueryBuilder groupchats = db.conversation.select()
|
||||||
|
.join_with(db.jid, db.jid.id, db.conversation.jid_id)
|
||||||
|
.join_with(db.account, db.account.id, db.conversation.account_id)
|
||||||
|
.with(db.jid.bare_jid, "LIKE", @"%$current_in%")
|
||||||
|
.with(db.account.enabled, "=", true)
|
||||||
|
.with(db.conversation.type_, "=", Conversation.Type.GROUPCHAT)
|
||||||
|
.order_by(db.conversation.last_active, "DESC")
|
||||||
|
.limit(limit);
|
||||||
|
foreach(Row chat in groupchats) {
|
||||||
|
suggestions.add(new SearchSuggestion(new Account.from_row(db, chat), new Jid(chat[db.jid.bare_jid]), "in:"+chat[db.jid.bare_jid], after_prev_space, next_space));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Other auto complete?
|
||||||
|
}
|
||||||
|
return suggestions;
|
||||||
|
}
|
||||||
|
|
||||||
public Gee.List<MessageItem> match_messages(string query, int offset = -1) {
|
public Gee.List<MessageItem> match_messages(string query, int offset = -1) {
|
||||||
Gee.List<MessageItem> ret = new ArrayList<MessageItem>();
|
Gee.List<MessageItem> ret = new ArrayList<MessageItem>();
|
||||||
var rows = prepare_search(query, true).limit(10);
|
QueryBuilder rows = prepare_search(query, false).limit(10);
|
||||||
if (offset > 0) {
|
if (offset > 0) {
|
||||||
rows.offset(offset);
|
rows.offset(offset);
|
||||||
}
|
}
|
||||||
|
@ -109,4 +243,21 @@ public class SearchProcessor : StreamInteractionModule, Object {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class SearchSuggestion : Object {
|
||||||
|
public Account account { get; private set; }
|
||||||
|
public Jid? jid { get; private set; }
|
||||||
|
public string completion { get; private set; }
|
||||||
|
public int start_index { get; private set; }
|
||||||
|
public int end_index { get; private set; }
|
||||||
|
public long order { get; set; }
|
||||||
|
|
||||||
|
public SearchSuggestion(Account account, Jid? jid, string completion, int start_index, int end_index) {
|
||||||
|
this.account = account;
|
||||||
|
this.jid = jid;
|
||||||
|
this.completion = completion;
|
||||||
|
this.start_index = start_index;
|
||||||
|
this.end_index = end_index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,7 @@ set(RESOURCE_LIST
|
||||||
menu_encryption.ui
|
menu_encryption.ui
|
||||||
occupant_list.ui
|
occupant_list.ui
|
||||||
occupant_list_item.ui
|
occupant_list_item.ui
|
||||||
|
search_autocomplete.ui
|
||||||
settings_dialog.ui
|
settings_dialog.ui
|
||||||
unified_main_content.ui
|
unified_main_content.ui
|
||||||
unified_window_placeholder.ui
|
unified_window_placeholder.ui
|
||||||
|
|
|
@ -1,144 +1,167 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<interface>
|
<interface>
|
||||||
<template class="DinoUiGlobalSearch" parent="GtkBox">
|
<template class="DinoUiGlobalSearch" parent="GtkOverlay">
|
||||||
<property name="orientation">vertical</property>
|
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkSearchEntry" id="search_entry">
|
<object class="GtkBox">
|
||||||
<property name="visible">True</property>
|
<property name="orientation">vertical</property>
|
||||||
<property name="margin">12</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkStack" id="results_empty_stack">
|
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkBox">
|
<object class="GtkSearchEntry" id="search_entry">
|
||||||
<property name="orientation">vertical</property>
|
|
||||||
<property name="spacing">10</property>
|
|
||||||
<property name="valign">center</property>
|
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<child>
|
<property name="margin">12</property>
|
||||||
<object class="GtkImage">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="icon-name">system-search-symbolic</property>
|
|
||||||
<property name="icon-size">4</property>
|
|
||||||
<property name="pixel-size">72</property>
|
|
||||||
<style>
|
|
||||||
<class name="dim-label"/>
|
|
||||||
</style>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel">
|
|
||||||
<property name="label" translatable="yes">No active search</property>
|
|
||||||
<property name="xalign">0.5</property>
|
|
||||||
<property name="yalign">0.5</property>
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<attributes>
|
|
||||||
<attribute name="weight" value="PANGO_WEIGHT_BOLD"/>
|
|
||||||
<attribute name="scale" value="1.3"/>
|
|
||||||
</attributes>
|
|
||||||
<style>
|
|
||||||
<class name="dim-label"/>
|
|
||||||
</style>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel">
|
|
||||||
<property name="label" translatable="yes">Type to start a search</property>
|
|
||||||
<property name="xalign">0.5</property>
|
|
||||||
<property name="yalign">0.5</property>
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<style>
|
|
||||||
<class name="dim-label"/>
|
|
||||||
</style>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
|
||||||
<property name="name">empty</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkBox">
|
<object class="GtkStack" id="results_empty_stack">
|
||||||
<property name="orientation">vertical</property>
|
|
||||||
<property name="spacing">10</property>
|
|
||||||
<property name="valign">center</property>
|
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkImage">
|
<object class="GtkBox">
|
||||||
<property name="visible">True</property>
|
<property name="orientation">vertical</property>
|
||||||
<property name="icon-name">face-uncertain-symbolic</property>
|
<property name="spacing">10</property>
|
||||||
<property name="icon-size">4</property>
|
<property name="valign">center</property>
|
||||||
<property name="pixel-size">72</property>
|
|
||||||
<style>
|
|
||||||
<class name="dim-label"/>
|
|
||||||
</style>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel">
|
|
||||||
<property name="label" translatable="yes">No matching messages</property>
|
|
||||||
<property name="xalign">0.5</property>
|
|
||||||
<property name="yalign">0.5</property>
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<attributes>
|
|
||||||
<attribute name="weight" value="PANGO_WEIGHT_BOLD"/>
|
|
||||||
<attribute name="scale" value="1.3"/>
|
|
||||||
</attributes>
|
|
||||||
<style>
|
|
||||||
<class name="dim-label"/>
|
|
||||||
</style>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel">
|
|
||||||
<property name="label" translatable="yes">Check the spelling or try to remove filters</property>
|
|
||||||
<property name="xalign">0.5</property>
|
|
||||||
<property name="yalign">0.5</property>
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<style>
|
|
||||||
<class name="dim-label"/>
|
|
||||||
</style>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="name">no-result</property>
|
|
||||||
</packing>
|
|
||||||
</child>z
|
|
||||||
<child>
|
|
||||||
<object class="GtkBox">
|
|
||||||
<property name="orientation">vertical</property>
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel" id="entry_number_label">
|
|
||||||
<property name="xalign">0</property>
|
|
||||||
<property name="use-markup">True</property>
|
|
||||||
<property name="margin-left">17</property>
|
|
||||||
<property name="visible">True</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkScrolledWindow" id="results_scrolled">
|
|
||||||
<property name="hscrollbar-policy">never</property>
|
|
||||||
<property name="expand">True</property>
|
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkBox" id="results_box">
|
<object class="GtkImage">
|
||||||
<property name="orientation">vertical</property>
|
|
||||||
<property name="spacing">25</property>
|
|
||||||
<property name="margin">10</property>
|
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
<property name="icon-name">system-search-symbolic</property>
|
||||||
|
<property name="icon-size">4</property>
|
||||||
|
<property name="pixel-size">72</property>
|
||||||
|
<style>
|
||||||
|
<class name="dim-label"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="label" translatable="yes">No active search</property>
|
||||||
|
<property name="xalign">0.5</property>
|
||||||
|
<property name="yalign">0.5</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<attributes>
|
||||||
|
<attribute name="weight" value="PANGO_WEIGHT_BOLD"/>
|
||||||
|
<attribute name="scale" value="1.3"/>
|
||||||
|
</attributes>
|
||||||
|
<style>
|
||||||
|
<class name="dim-label"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="label" translatable="yes">Type to start a search</property>
|
||||||
|
<property name="xalign">0.5</property>
|
||||||
|
<property name="yalign">0.5</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<style>
|
||||||
|
<class name="dim-label"/>
|
||||||
|
</style>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="name">empty</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<property name="spacing">10</property>
|
||||||
|
<property name="valign">center</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImage">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="icon-name">face-uncertain-symbolic</property>
|
||||||
|
<property name="icon-size">4</property>
|
||||||
|
<property name="pixel-size">72</property>
|
||||||
|
<style>
|
||||||
|
<class name="dim-label"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="label" translatable="yes">No matching messages</property>
|
||||||
|
<property name="xalign">0.5</property>
|
||||||
|
<property name="yalign">0.5</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<attributes>
|
||||||
|
<attribute name="weight" value="PANGO_WEIGHT_BOLD"/>
|
||||||
|
<attribute name="scale" value="1.3"/>
|
||||||
|
</attributes>
|
||||||
|
<style>
|
||||||
|
<class name="dim-label"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel">
|
||||||
|
<property name="label" translatable="yes">Check the spelling or try to remove filters</property>
|
||||||
|
<property name="xalign">0.5</property>
|
||||||
|
<property name="yalign">0.5</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<style>
|
||||||
|
<class name="dim-label"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="name">no-result</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel" id="entry_number_label">
|
||||||
|
<property name="xalign">0</property>
|
||||||
|
<property name="use-markup">True</property>
|
||||||
|
<property name="margin-left">17</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkScrolledWindow" id="results_scrolled">
|
||||||
|
<property name="hscrollbar-policy">never</property>
|
||||||
|
<property name="expand">True</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox" id="results_box">
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<property name="spacing">25</property>
|
||||||
|
<property name="margin">10</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="name">results</property>
|
||||||
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
</child>
|
||||||
<property name="name">results</property>
|
</object>
|
||||||
</packing>
|
</child>
|
||||||
|
<child type="overlay">
|
||||||
|
<object class="GtkFrame" id="auto_complete_overlay">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="margin-top">42</property>
|
||||||
|
<property name="margin-left">12</property>
|
||||||
|
<property name="margin-right">12</property>
|
||||||
|
<property name="valign">start</property>
|
||||||
|
<style>
|
||||||
|
<class name="auto-complete"/>
|
||||||
|
</style>
|
||||||
|
<child>
|
||||||
|
<object class="GtkListBox" id="auto_complete_list">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="selection-mode">browse</property>
|
||||||
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
|
|
24
main/data/search_autocomplete.ui
Normal file
24
main/data/search_autocomplete.ui
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<interface>
|
||||||
|
<object class="GtkBox" id="root">
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<child>
|
||||||
|
<object class="DinoUiAvatarImage" id="image">
|
||||||
|
<property name="margin">4</property>
|
||||||
|
<property name="margin-start">6</property>
|
||||||
|
<property name="margin-end">6</property>
|
||||||
|
<property name="height">24</property>
|
||||||
|
<property name="width">24</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="allow_gray">False</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel" id="label">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="ellipsize">end</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</interface>
|
|
@ -33,28 +33,31 @@ window.dino-main .dino-conversation textview, window.dino-main .dino-conversatio
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.dino-main .dino-sidebar frame {
|
window.dino-main .dino-sidebar > frame {
|
||||||
background: @insensitive_bg_color;
|
background: @insensitive_bg_color;
|
||||||
border-left: 1px solid @borders;
|
border-left: 1px solid @borders;
|
||||||
border-bottom: 1px solid @borders;
|
border-bottom: 1px solid @borders;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.dino-main .dino-sidebar frame.collapsed {
|
window.dino-main .dino-sidebar > frame.collapsed {
|
||||||
border-bottom: 1px solid @borders;
|
border-bottom: 1px solid @borders;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.dino-main .dino-sidebar frame.auto-complete {
|
||||||
|
background: @theme_base_color;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.dino-main .dino-sidebar frame.auto-complete list > row {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
window.dino-main .dino-sidebar textview,
|
window.dino-main .dino-sidebar textview,
|
||||||
window.dino-main .dino-sidebar textview text {
|
window.dino-main .dino-sidebar textview text {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
window.dino-main .dino-chatinput frame box {
|
window.dino-main .dino-chatinput frame box {
|
||||||
background: @theme_base_color;
|
background: transparent;
|
||||||
}
|
|
||||||
|
|
||||||
window.dino-main .dino-chatinput frame box:backdrop {
|
|
||||||
background: @theme_unfocused_base_color;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.dino-main button.dino-chatinput-button {
|
window.dino-main button.dino-chatinput-button {
|
||||||
|
|
|
@ -44,13 +44,13 @@
|
||||||
<property name="valign">end</property>
|
<property name="valign">end</property>
|
||||||
<property name="transition-type">crossfade</property>
|
<property name="transition-type">crossfade</property>
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
<property name="margin-end">30</property>
|
||||||
|
<property name="margin-bottom">70</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkButton" id="goto_end_button">
|
<object class="GtkButton" id="goto_end_button">
|
||||||
<property name="vexpand">False</property>
|
<property name="vexpand">False</property>
|
||||||
<property name="halign">end</property>
|
<property name="halign">end</property>
|
||||||
<property name="valign">end</property>
|
<property name="valign">end</property>
|
||||||
<property name="margin-end">70</property>
|
|
||||||
<property name="margin-bottom">100</property>
|
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<style>
|
<style>
|
||||||
<class name="circular"/>
|
<class name="circular"/>
|
||||||
|
|
|
@ -7,9 +7,8 @@ using Dino.Entities;
|
||||||
namespace Dino.Ui {
|
namespace Dino.Ui {
|
||||||
|
|
||||||
[GtkTemplate (ui = "/im/dino/Dino/global_search.ui")]
|
[GtkTemplate (ui = "/im/dino/Dino/global_search.ui")]
|
||||||
class GlobalSearch : Box {
|
class GlobalSearch : Overlay {
|
||||||
public signal void selected_item(MessageItem item);
|
public signal void selected_item(MessageItem item);
|
||||||
|
|
||||||
private StreamInteractor stream_interactor;
|
private StreamInteractor stream_interactor;
|
||||||
private string search = "";
|
private string search = "";
|
||||||
private int loaded_results = -1;
|
private int loaded_results = -1;
|
||||||
|
@ -20,6 +19,8 @@ class GlobalSearch : Box {
|
||||||
[GtkChild] public ScrolledWindow results_scrolled;
|
[GtkChild] public ScrolledWindow results_scrolled;
|
||||||
[GtkChild] public Box results_box;
|
[GtkChild] public Box results_box;
|
||||||
[GtkChild] public Stack results_empty_stack;
|
[GtkChild] public Stack results_empty_stack;
|
||||||
|
[GtkChild] public Frame auto_complete_overlay;
|
||||||
|
[GtkChild] public ListBox auto_complete_list;
|
||||||
|
|
||||||
public GlobalSearch init(StreamInteractor stream_interactor) {
|
public GlobalSearch init(StreamInteractor stream_interactor) {
|
||||||
this.stream_interactor = stream_interactor;
|
this.stream_interactor = stream_interactor;
|
||||||
|
@ -27,6 +28,8 @@ class GlobalSearch : Box {
|
||||||
search_entry.search_changed.connect(() => {
|
search_entry.search_changed.connect(() => {
|
||||||
set_search(search_entry.text);
|
set_search(search_entry.text);
|
||||||
});
|
});
|
||||||
|
search_entry.notify["text"].connect_after(() => { update_auto_complete(); });
|
||||||
|
search_entry.notify["cursor-position"].connect_after(() => { update_auto_complete(); });
|
||||||
|
|
||||||
results_scrolled.vadjustment.notify["value"].connect(() => {
|
results_scrolled.vadjustment.notify["value"].connect(() => {
|
||||||
if (results_scrolled.vadjustment.upper - (results_scrolled.vadjustment.value + results_scrolled.vadjustment.page_size) < 100) {
|
if (results_scrolled.vadjustment.upper - (results_scrolled.vadjustment.value + results_scrolled.vadjustment.page_size) < 100) {
|
||||||
|
@ -44,9 +47,70 @@ class GlobalSearch : Box {
|
||||||
reloading_mutex.trylock();
|
reloading_mutex.trylock();
|
||||||
reloading_mutex.unlock();
|
reloading_mutex.unlock();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 + " ");
|
||||||
|
}
|
||||||
|
|
||||||
private void clear_search() {
|
private void clear_search() {
|
||||||
results_box.@foreach((widget) => { widget.destroy(); });
|
results_box.@foreach((widget) => { widget.destroy(); });
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,9 @@ public class QueryBuilder : StatementBuilder {
|
||||||
// ORDER BY [...]
|
// ORDER BY [...]
|
||||||
private OrderingTerm[]? order_by_terms = {};
|
private OrderingTerm[]? order_by_terms = {};
|
||||||
|
|
||||||
|
// GROUP BY [...]
|
||||||
|
private string? group_by_term;
|
||||||
|
|
||||||
// LIMIT [...] OFFSET [...]
|
// LIMIT [...] OFFSET [...]
|
||||||
private int limit_val;
|
private int limit_val;
|
||||||
private int offset_val;
|
private int offset_val;
|
||||||
|
@ -125,6 +128,17 @@ public class QueryBuilder : StatementBuilder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public QueryBuilder group_by(Column[] columns) {
|
||||||
|
foreach(Column col in columns) {
|
||||||
|
if (group_by_term == null) {
|
||||||
|
group_by_term = col.to_string();
|
||||||
|
} else {
|
||||||
|
group_by_term += @", $col";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public QueryBuilder limit(int limit) {
|
public QueryBuilder limit(int limit) {
|
||||||
if (this.limit_val != 0 && limit > this.limit_val) error("tried to increase an existing limit");
|
if (this.limit_val != 0 && limit > this.limit_val) error("tried to increase an existing limit");
|
||||||
this.limit_val = limit;
|
this.limit_val = limit;
|
||||||
|
@ -162,7 +176,7 @@ public class QueryBuilder : StatementBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
internal override Statement prepare() {
|
internal override Statement prepare() {
|
||||||
Statement stmt = db.prepare(@"SELECT $column_selector $(table_name == null ? "" : @"FROM $((!) table_name)") $joins WHERE $selection $(OrderingTerm.all_to_string(order_by_terms)) $(limit_val > 0 ? @" LIMIT $limit_val OFFSET $offset_val" : "")");
|
Statement stmt = db.prepare(@"SELECT $column_selector $(table_name == null ? "" : @"FROM $((!) table_name)") $joins WHERE $selection $(group_by_term == null ? "" : @"GROUP BY $group_by_term") $(OrderingTerm.all_to_string(order_by_terms)) $(limit_val > 0 ? @" LIMIT $limit_val OFFSET $offset_val" : "")");
|
||||||
for (int i = 0; i < selection_args.length; i++) {
|
for (int i = 0; i < selection_args.length; i++) {
|
||||||
selection_args[i].bind(stmt, i+1);
|
selection_args[i].bind(stmt, i+1);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue