From c0314212a0d951494fe6397fa53a9c5689a3ff87 Mon Sep 17 00:00:00 2001 From: fiaxh Date: Fri, 24 Mar 2017 00:15:00 +0100 Subject: Tab completion for MUC occupants --- .../src/ui/chat_input/occupants_tab_completer.vala | 116 +++++++++++++++++++++ main/src/ui/chat_input/smiley_converter.vala | 58 +++++++++++ main/src/ui/chat_input/view.vala | 102 ++++++++++++++++++ 3 files changed, 276 insertions(+) create mode 100644 main/src/ui/chat_input/occupants_tab_completer.vala create mode 100644 main/src/ui/chat_input/smiley_converter.vala create mode 100644 main/src/ui/chat_input/view.vala (limited to 'main/src/ui/chat_input') diff --git a/main/src/ui/chat_input/occupants_tab_completer.vala b/main/src/ui/chat_input/occupants_tab_completer.vala new file mode 100644 index 00000000..93b0e7ff --- /dev/null +++ b/main/src/ui/chat_input/occupants_tab_completer.vala @@ -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 completions = new ArrayList(); + 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 generate_completions_from_messages() { + Gee.List ret = new ArrayList(); + Gee.List? 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 generate_completions_from_occupants(string prefix) { + Gee.List ret = new ArrayList(); + Gee.List? 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; + } +} + +} \ No newline at end of file diff --git a/main/src/ui/chat_input/smiley_converter.vala b/main/src/ui/chat_input/smiley_converter.vala new file mode 100644 index 00000000..73413972 --- /dev/null +++ b/main/src/ui/chat_input/smiley_converter.vala @@ -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 smiley_translations = new HashMap(); + + 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]; + } + } + } + } + } +} + +} \ No newline at end of file diff --git a/main/src/ui/chat_input/view.vala b/main/src/ui/chat_input/view.vala new file mode 100644 index 00000000..310c0f35 --- /dev/null +++ b/main/src/ui/chat_input/view.vala @@ -0,0 +1,102 @@ +using Gdk; +using Gee; +using Gtk; + +using Dino.Entities; +using Xmpp; + +namespace Dino.Ui.ChatInput { + +[GtkTemplate (ui = "/org/dino-im/chat_input.ui")] +public class View : Box { + + [GtkChild] private ScrolledWindow scrolled; + [GtkChild] private TextView text_input; + + private StreamInteractor stream_interactor; + private Conversation? conversation; + private HashMap entry_cache = new HashMap(Conversation.hash_func, Conversation.equals_func); + private int vscrollbar_min_height; + private OccupantsTabCompletor occupants_tab_completor; + private SmileyConverter smiley_converter; + + 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); + text_input.buffer.changed.connect(on_text_input_changed); + } + + 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; + + text_input.buffer.changed.disconnect(on_text_input_changed); + text_input.buffer.text = ""; + if (entry_cache.has_key(conversation)) { + text_input.buffer.text = entry_cache[conversation]; + } + text_input.buffer.changed.connect(on_text_input_changed); + + text_input.grab_focus(); + } + + private void send_text() { + string text = text_input.buffer.text; + if (text.has_prefix("/")) { + string[] token = text.split(" ", 2); + switch(token[0]) { + case "/kick": + stream_interactor.get_module(MucManager.IDENTITY).kick(conversation.account, conversation.counterpart, token[1]); + break; + case "/me": + stream_interactor.get_module(MessageManager.IDENTITY).send_message(text, conversation); + break; + case "/nick": + stream_interactor.get_module(MucManager.IDENTITY).change_nick(conversation.account, conversation.counterpart, token[1]); + break; + case "/topic": + stream_interactor.get_module(MucManager.IDENTITY).change_subject(conversation.account, conversation.counterpart, token[1]); + break; + } + } else { + stream_interactor.get_module(MessageManager.IDENTITY).send_message(text, conversation); + } + text_input.buffer.text = ""; + } + + private bool on_text_input_key_press(EventKey event) { + if (event.keyval == Key.Return) { + if ((event.state & ModifierType.SHIFT_MASK) > 0) { + text_input.buffer.insert_at_cursor("\n", 1); + } else if (text_input.buffer.text != ""){ + send_text(); + } + return true; + } + return false; + } + + private void on_upper_notify() { + scrolled.vadjustment.value = scrolled.vadjustment.upper - scrolled.vadjustment.page_size; + + // hack for vscrollbar not requiring space and making textview higher //TODO doesn't resize immediately + scrolled.get_vscrollbar().visible = (scrolled.vadjustment.upper > scrolled.max_content_height - 2 * vscrollbar_min_height); + } + + private void on_text_input_changed() { + if (text_input.buffer.text != "") { + stream_interactor.get_module(ChatInteraction.IDENTITY).on_message_entered(conversation); + } else { + stream_interactor.get_module(ChatInteraction.IDENTITY).on_message_cleared(conversation); + } + } +} + +} \ No newline at end of file -- cgit v1.2.3-54-g00ecf