Tab completion for MUC occupants

This commit is contained in:
fiaxh 2017-03-24 00:15:00 +01:00
parent 5862e25337
commit c0314212a0
9 changed files with 199 additions and 44 deletions

View file

@ -41,13 +41,13 @@ public class MessageManager : StreamInteractionModule, Object {
message_sent(message, conversation);
}
public Gee.List<Entities.Message>? get_messages(Conversation conversation) {
public Gee.List<Entities.Message>? get_messages(Conversation conversation, int count = 50) {
if (messages.has_key(conversation) && messages[conversation].size > 0) {
Gee.List<Entities.Message> db_messages = db.get_messages(conversation.counterpart, conversation.account, 50, messages[conversation][0]);
Gee.List<Entities.Message> db_messages = db.get_messages(conversation.counterpart, conversation.account, count, messages[conversation][0]);
db_messages.add_all(messages[conversation]);
return db_messages;
} else {
Gee.List<Entities.Message> db_messages = db.get_messages(conversation.counterpart, conversation.account, 50, null);
Gee.List<Entities.Message> db_messages = db.get_messages(conversation.counterpart, conversation.account, count, null);
return db_messages;
}
}

View file

@ -53,7 +53,10 @@ public class MucManager : StreamInteractionModule, Object {
}
public ArrayList<Jid>? get_occupants(Jid jid, Account account) {
return stream_interactor.get_module(PresenceManager.IDENTITY).get_full_jids(jid, account);
if (is_groupchat(jid, account)) {
return stream_interactor.get_module(PresenceManager.IDENTITY).get_full_jids(jid, account);
}
return null;
}
public ArrayList<Jid>? get_other_occupants(Jid jid, Account account) {

View file

@ -68,7 +68,9 @@ SOURCES
src/ui/add_conversation/list_row.vala
src/ui/add_conversation/select_jid_fragment.vala
src/ui/avatar_generator.vala
src/ui/chat_input.vala
src/ui/chat_input/occupants_tab_completer.vala
src/ui/chat_input/smiley_converter.vala
src/ui/chat_input/view.vala
src/ui/conversation_list_titlebar.vala
src/ui/conversation_selector/chat_row.vala
src/ui/conversation_selector/conversation_row.vala

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk+" version="3.22"/>
<template class="DinoUiChatInput">
<template class="DinoUiChatInputView">
<property name="hexpand">True</property>
<property name="orientation">horizontal</property>
<property name="margin">5</property>

View file

@ -11,6 +11,7 @@
<property name="visible">True</property>
<child>
<object class="GtkScrolledWindow" id="scrolled">
<property name="hscrollbar_policy">never</property>
<property name="visible">True</property>
<child>
<object class="GtkBox">

View file

@ -0,0 +1,116 @@
using Gdk;
using Gee;
using Gtk;
using Dino.Entities;
namespace Dino.Ui.ChatInput {
/**
* - With given prefix: Complete from occupant list (sorted lexicographically)
* - W/o prefix: Complete from received messages (most recent first)
* - At the start (with ",") and in the middle of a text
* - Backwards tabbing
*/
class OccupantsTabCompletor {
private StreamInteractor stream_interactor;
private Conversation? conversation;
private TextView text_input;
private Gee.List<string> completions = new ArrayList<string>();
private bool active = false;
private int index = -1;
public OccupantsTabCompletor(StreamInteractor stream_interactor, TextView text_input) {
this.stream_interactor = stream_interactor;
this.text_input = text_input;
text_input.key_press_event.connect(on_text_input_key_press);
}
public void initialize_for_conversation(Conversation conversation) {
this.conversation = conversation;
}
public bool on_text_input_key_press(EventKey event) {
if (conversation.type_ == Conversation.Type.GROUPCHAT) {
if (event.keyval == Key.Tab || event.keyval == Key.ISO_Left_Tab) {
string text = text_input.buffer.text;
int start_index = int.max(text.last_index_of(" "), text.last_index_of("\n")) + 1;
string word = text.substring(start_index);
if (!active) {
if (word == "") {
completions = generate_completions_from_messages();
} else {
completions = generate_completions_from_occupants(word);
}
if (completions.size > 0) {
active = true;
index = -1;
}
}
if (event.keyval != Key.ISO_Group_Shift && active) {
text_input.buffer.text = next_completion(event.keyval == Key.ISO_Left_Tab);
return true;
}
} else if (event.keyval != Key.Shift_L && active) {
active = false;
}
}
return false;
}
private string next_completion(bool backwards) {
string text = text_input.buffer.text;
int start_index = int.max(text.last_index_of(" "), text.last_index_of("\n")) + 1;
string prev_completion = text.substring(start_index);
if (index > -1) {
start_index = int.max(
text.substring(0, text.length - 1).last_index_of(" "),
text.substring(0, text.length - 1).last_index_of("\n")
) + 1;
prev_completion = text.substring(start_index);
}
if (backwards) {
index = int.max(index, 0) - 1;
if (index < 0) index = completions.size - 1;
} else {
index = (index + 1) % (completions.size);
}
if (start_index == 0) {
return completions[index] + ", ";
} else {
return text.substring(0, text.length - prev_completion.length) + completions[index] + " ";
}
}
private Gee.List<string> generate_completions_from_messages() {
Gee.List<string> ret = new ArrayList<string>();
Gee.List<Message>? messages = stream_interactor.get_module(MessageManager.IDENTITY).get_messages(conversation, 10);
if (messages != null) {
for (int i = messages.size - 1; i > 0; i--) {
string resourcepart = messages[i].from.resourcepart;
string own_nick = stream_interactor.get_module(MucManager.IDENTITY).get_nick(conversation.counterpart, conversation.account);
if (resourcepart != null && resourcepart != "" && resourcepart != own_nick && !ret.contains(resourcepart)) {
ret.add(resourcepart);
}
}
}
return ret;
}
private Gee.List<string> generate_completions_from_occupants(string prefix) {
Gee.List<string> ret = new ArrayList<string>();
Gee.List<Jid>? occupants = stream_interactor.get_module(MucManager.IDENTITY).get_other_occupants(conversation.counterpart, conversation.account);
if (occupants != null) {
foreach (Jid jid in occupants) {
if (jid.resourcepart.to_string().has_prefix(prefix)) ret.add(jid.resourcepart.to_string());
}
}
ret.sort();
return ret;
}
}
}

View file

@ -0,0 +1,58 @@
using Gdk;
using Gee;
using Gtk;
using Dino.Entities;
namespace Dino.Ui.ChatInput {
class SmileyConverter {
private StreamInteractor stream_interactor;
private TextView text_input;
private static HashMap<string, string> smiley_translations = new HashMap<string, string>();
static construct {
smiley_translations[":)"] = "🙂";
smiley_translations[":D"] = "😀";
smiley_translations[";)"] = "😉";
smiley_translations["O:)"] = "😇";
smiley_translations["O:-)"] = "😇";
smiley_translations["]:>"] = "😈";
smiley_translations[":o"] = "😮";
smiley_translations[":P"] = "😛";
smiley_translations[";P"] = "😜";
smiley_translations[":("] = "🙁";
smiley_translations[":'("] = "😢";
smiley_translations[":/"] = "😕";
}
public SmileyConverter(StreamInteractor stream_interactor, TextView text_input) {
this.stream_interactor = stream_interactor;
this.text_input = text_input;
text_input.key_press_event.connect(on_text_input_key_press);
}
public bool on_text_input_key_press(EventKey event) {
if (event.keyval == Key.space || event.keyval == Key.Return) {
check_convert();
}
return false;
}
private void check_convert() {
if (Dino.Settings.instance().convert_utf8_smileys) {
foreach (string smiley in smiley_translations.keys) {
if (text_input.buffer.text.has_suffix(smiley)) {
if (text_input.buffer.text.length == smiley.length ||
text_input.buffer.text[text_input.buffer.text.length - smiley.length - 1] == ' ') {
text_input.buffer.text = text_input.buffer.text.substring(0, text_input.buffer.text.length - smiley.length) + smiley_translations[smiley];
}
}
}
}
}
}
}

View file

@ -5,37 +5,26 @@ using Gtk;
using Dino.Entities;
using Xmpp;
namespace Dino.Ui {
namespace Dino.Ui.ChatInput {
[GtkTemplate (ui = "/org/dino-im/chat_input.ui")]
public class ChatInput : Box {
public class View : Box {
[GtkChild] private ScrolledWindow scrolled;
[GtkChild] private TextView text_input;
private Conversation? conversation;
private StreamInteractor stream_interactor;
private Conversation? conversation;
private HashMap<Conversation, string> entry_cache = new HashMap<Conversation, string>(Conversation.hash_func, Conversation.equals_func);
private static HashMap<string, string> smiley_translations = new HashMap<string, string>();
private int vscrollbar_min_height;
private OccupantsTabCompletor occupants_tab_completor;
private SmileyConverter smiley_converter;
static construct {
smiley_translations[":)"] = "🙂";
smiley_translations[":D"] = "😀";
smiley_translations[";)"] = "😉";
smiley_translations["O:)"] = "😇";
smiley_translations["]:>"] = "😈";
smiley_translations[":o"] = "😮";
smiley_translations[":P"] = "😛";
smiley_translations[";P"] = "😜";
smiley_translations[":("] = "🙁";
smiley_translations[":'("] = "😢";
smiley_translations[":/"] = "😕";
smiley_translations["-.-"] = "😑";
}
public ChatInput(StreamInteractor stream_interactor) {
public View(StreamInteractor stream_interactor) {
this.stream_interactor = stream_interactor;
occupants_tab_completor = new OccupantsTabCompletor(stream_interactor, text_input);
smiley_converter = new SmileyConverter(stream_interactor, text_input);
scrolled.get_vscrollbar().get_preferred_height(out vscrollbar_min_height, null);
scrolled.vadjustment.notify["upper"].connect_after(on_upper_notify);
text_input.key_press_event.connect(on_text_input_key_press);
@ -43,6 +32,8 @@ public class ChatInput : Box {
}
public void initialize_for_conversation(Conversation conversation) {
occupants_tab_completor.initialize_for_conversation(conversation);
if (this.conversation != null) entry_cache[this.conversation] = text_input.buffer.text;
this.conversation = conversation;
@ -81,9 +72,6 @@ public class ChatInput : Box {
}
private bool on_text_input_key_press(EventKey event) {
if (event.keyval == Key.space || event.keyval == Key.Return) {
check_convert_smiley();
}
if (event.keyval == Key.Return) {
if ((event.state & ModifierType.SHIFT_MASK) > 0) {
text_input.buffer.insert_at_cursor("\n", 1);
@ -102,19 +90,6 @@ public class ChatInput : Box {
scrolled.get_vscrollbar().visible = (scrolled.vadjustment.upper > scrolled.max_content_height - 2 * vscrollbar_min_height);
}
private void check_convert_smiley() {
if (Dino.Settings.instance().convert_utf8_smileys) {
foreach (string smiley in smiley_translations.keys) {
if (text_input.buffer.text.has_suffix(smiley)) {
if (text_input.buffer.text.length == smiley.length ||
text_input.buffer.text[text_input.buffer.text.length - smiley.length - 1] == ' ') {
text_input.buffer.text = text_input.buffer.text.substring(0, text_input.buffer.text.length - smiley.length) + smiley_translations[smiley];
}
}
}
}
}
private void on_text_input_changed() {
if (text_input.buffer.text != "") {
stream_interactor.get_module(ChatInteraction.IDENTITY).on_message_entered(conversation);

View file

@ -9,7 +9,7 @@ public class UnifiedWindow : Window {
private NoAccountsPlaceholder accounts_placeholder = new NoAccountsPlaceholder() { visible=true };
private NoConversationsPlaceholder conversations_placeholder = new NoConversationsPlaceholder() { visible=true };
private ChatInput chat_input;
private ChatInput.View chat_input;
private ConversationListTitlebar conversation_list_titlebar;
private ConversationSelector.View filterable_conversation_list;
private ConversationSummary.View conversation_frame;
@ -62,7 +62,7 @@ public class UnifiedWindow : Window {
}
private void setup_unified() {
chat_input = new ChatInput(stream_interactor) { visible=true };
chat_input = new ChatInput.View(stream_interactor) { visible=true };
conversation_frame = new ConversationSummary.View(stream_interactor) { visible=true };
filterable_conversation_list = new ConversationSelector.View(stream_interactor) { visible=true };