diff options
author | ouwou <26526779+ouwou@users.noreply.github.com> | 2020-12-20 22:10:45 -0500 |
---|---|---|
committer | ouwou <26526779+ouwou@users.noreply.github.com> | 2020-12-20 22:10:45 -0500 |
commit | 31bef80530edbf622a6678947dc832bc179b52fb (patch) | |
tree | 549a148c0530f3f598932f11e3655199e4bfdb7b /components | |
parent | c02cfb9dd8a32da233f9dcbfd9616d03034d12af (diff) | |
download | abaddon-portaudio-31bef80530edbf622a6678947dc832bc179b52fb.tar.gz abaddon-portaudio-31bef80530edbf622a6678947dc832bc179b52fb.zip |
add mention/emoji/channel completion
Diffstat (limited to 'components')
-rw-r--r-- | components/chatwindow.cpp | 35 | ||||
-rw-r--r-- | components/chatwindow.hpp | 3 | ||||
-rw-r--r-- | components/completer.cpp | 323 | ||||
-rw-r--r-- | components/completer.hpp | 61 |
4 files changed, 422 insertions, 0 deletions
diff --git a/components/chatwindow.cpp b/components/chatwindow.cpp index 25492f8..71b0fb0 100644 --- a/components/chatwindow.cpp +++ b/components/chatwindow.cpp @@ -52,9 +52,41 @@ ChatWindow::ChatWindow() { m_input_scroll->set_max_content_height(200); m_input_scroll->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + m_completer.SetBuffer(m_input->get_buffer()); + m_completer.SetGetChannelID([this]() -> auto { + return m_active_channel; + }); + + m_completer.SetGetRecentAuthors([this]() -> auto { + const auto &discord = Abaddon::Get().GetDiscordClient(); + std::vector<Snowflake> ret; + + std::map<Snowflake, Gtk::Widget *> ordered(m_id_to_widget.begin(), m_id_to_widget.end()); + + for (auto it = ordered.crbegin(); it != ordered.crend(); it++) { + const auto *widget = dynamic_cast<ChatMessageItemContainer *>(it->second); + if (widget == nullptr) continue; + const auto msg = discord.GetMessage(widget->ID); + if (!msg.has_value()) continue; + if (std::find(ret.begin(), ret.end(), msg->Author.ID) == ret.end()) + ret.push_back(msg->Author.ID); + } + + const auto chan = discord.GetChannel(m_active_channel); + if (chan->GuildID.has_value()) { + const auto others = discord.GetUsersInGuild(*chan->GuildID); + for (const auto id : others) + if (std::find(ret.begin(), ret.end(), id) == ret.end()) + ret.push_back(id); + } + + return ret; + }); + m_input_scroll->add(*m_input); m_scroll->add(*m_list); m_main->add(*m_scroll); + m_main->add(m_completer); m_main->add(*m_input_scroll); } @@ -141,6 +173,9 @@ Snowflake ChatWindow::GetActiveChannel() const { } bool ChatWindow::on_key_press_event(GdkEventKey *e) { + if (m_completer.ProcessKeyPress(e)) + return true; + if (e->keyval == GDK_KEY_Return) { if (e->state & GDK_SHIFT_MASK) return false; diff --git a/components/chatwindow.hpp b/components/chatwindow.hpp index 823e988..419b69a 100644 --- a/components/chatwindow.hpp +++ b/components/chatwindow.hpp @@ -6,6 +6,7 @@ #include <set> #include "../discord/discord.hpp" #include "chatmessage.hpp" +#include "completer.hpp" class ChatWindow { public: @@ -65,6 +66,8 @@ protected: Gtk::TextView *m_input; Gtk::ScrolledWindow *m_input_scroll; + Completer m_completer; + public: typedef sigc::signal<void, Snowflake, Snowflake> type_signal_action_message_delete; typedef sigc::signal<void, Snowflake, Snowflake> type_signal_action_message_edit; diff --git a/components/completer.cpp b/components/completer.cpp new file mode 100644 index 0000000..d673d40 --- /dev/null +++ b/components/completer.cpp @@ -0,0 +1,323 @@ +#include "completer.hpp" +#include "../abaddon.hpp" +#include "../util.hpp" + +constexpr const int CompleterHeight = 150; +constexpr const int MaxCompleterEntries = 15; + +Completer::Completer() { + set_reveal_child(false); + set_transition_type(Gtk::REVEALER_TRANSITION_TYPE_NONE); // only SLIDE_UP and NONE work decently + + m_scroll.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); + m_scroll.set_max_content_height(CompleterHeight); + m_scroll.set_size_request(-1, CompleterHeight); + m_scroll.set_placement(Gtk::CORNER_BOTTOM_LEFT); + + m_list.set_adjustment(m_scroll.get_vadjustment()); + m_list.set_focus_vadjustment(m_scroll.get_vadjustment()); + m_list.get_style_context()->add_class("completer"); + m_list.set_activate_on_single_click(true); + + m_list.set_focus_on_click(false); + set_can_focus(false); + + m_list.signal_row_activated().connect(sigc::mem_fun(*this, &Completer::OnRowActivate)); + + m_scroll.add(m_list); + add(m_scroll); + show_all(); +} + +Completer::Completer(const Glib::RefPtr<Gtk::TextBuffer> &buf) + : Completer() { + SetBuffer(buf); +} + +void Completer::SetBuffer(const Glib::RefPtr<Gtk::TextBuffer> &buf) { + m_buf = buf; + m_buf->signal_changed().connect(sigc::mem_fun(*this, &Completer::OnTextBufferChanged)); +} + +bool Completer::ProcessKeyPress(GdkEventKey *e) { + if (!IsShown()) return false; + if (e->type != GDK_KEY_PRESS) return false; + + switch (e->keyval) { + case GDK_KEY_Down: { + if (m_entries.size() == 0) return true; + const int index = m_list.get_selected_row()->get_index(); + if (index >= m_entries.size() - 1) return true; + m_list.select_row(*m_entries[index + 1]); + ScrollListBoxToSelected(m_list); + } + return true; + case GDK_KEY_Up: { + if (m_entries.size() == 0) return true; + const int index = m_list.get_selected_row()->get_index(); + if (index == 0) return true; + m_list.select_row(*m_entries[index - 1]); + ScrollListBoxToSelected(m_list); + } + return true; + case GDK_KEY_Return: { + if (m_entries.size() == 0) return true; + DoCompletion(m_list.get_selected_row()); + } + return true; + default: + break; + } + + return false; +} + +void Completer::SetGetRecentAuthors(get_recent_authors_cb cb) { + m_recent_authors_cb = cb; +} + +void Completer::SetGetChannelID(get_channel_id_cb cb) { + m_channel_id_cb = cb; +} + +bool Completer::IsShown() const { + return get_child_revealed(); +} + +CompleterEntry *Completer::CreateEntry(const Glib::ustring &completion) { + auto entry = Gtk::manage(new CompleterEntry(completion, m_entries.size())); + m_entries.push_back(entry); + entry->show_all(); + m_list.add(*entry); + return entry; +} + +void Completer::CompleteMentions(const Glib::ustring &term) { + if (!m_recent_authors_cb) + return; + + const auto &discord = Abaddon::Get().GetDiscordClient(); + + Snowflake channel_id; + if (m_channel_id_cb) + channel_id = m_channel_id_cb(); + auto author_ids = m_recent_authors_cb(); + if (channel_id.IsValid()) { + const auto chan = discord.GetChannel(channel_id); + if (chan->GuildID.has_value()) { + const auto members = discord.GetUsersInGuild(*chan->GuildID); + for (const auto x : members) + if (std::find(author_ids.begin(), author_ids.end(), x) == author_ids.end()) + author_ids.push_back(x); + } + } + const auto me = discord.GetUserData().ID; + int i = 0; + for (const auto id : author_ids) { + if (id == me) continue; + const auto author = discord.GetUser(id); + if (!author.has_value()) continue; + if (!StringContainsCaseless(author->Username, term)) continue; + if (i++ > 15) break; + + auto entry = CreateEntry(author->GetMention()); + + entry->SetText(author->Username + "#" + author->Discriminator); + + if (channel_id.IsValid()) { + const auto chan = discord.GetChannel(channel_id); + if (chan.has_value() && chan->GuildID.has_value()) { + const auto role_id = discord.GetMemberHoistedRole(*chan->GuildID, id, true); + if (role_id.IsValid()) { + const auto role = discord.GetRole(role_id); + if (role.has_value()) + entry->SetTextColor(role->Color); + } + } + } + + auto &img = Abaddon::Get().GetImageManager(); + const auto placeholder = img.GetPlaceholder(24); + if (author->HasAvatar()) { + auto pb = img.GetFromURLIfCached(author->GetAvatarURL()); + if (pb) { + entry->SetImage(pb); + } else { + entry->SetImage(placeholder); + img.LoadFromURL(author->GetAvatarURL(), sigc::mem_fun(*entry, &CompleterEntry::SetImage)); + } + } else + entry->SetImage(placeholder); + } +} + +void Completer::CompleteEmojis(const Glib::ustring &term) { + if (!m_channel_id_cb) + return; + + const auto &discord = Abaddon::Get().GetDiscordClient(); + const auto channel_id = m_channel_id_cb(); + const auto channel = discord.GetChannel(channel_id); + if (!channel->GuildID.has_value()) return; + const auto guild = discord.GetGuild(*channel->GuildID); + + const auto make_entry = [&](const Glib::ustring &name, const Glib::ustring &completion, const Glib::ustring &url = "") -> CompleterEntry * { + const auto entry = CreateEntry(completion); + entry->SetText(name); + if (url == "") return entry; + auto &img = Abaddon::Get().GetImageManager(); + const auto placeholder = img.GetPlaceholder(24); + const auto pb = img.GetFromURLIfCached(url); + if (pb) + entry->SetImage(pb); + else { + entry->SetImage(placeholder); + img.LoadFromURL(url, sigc::mem_fun(*entry, &CompleterEntry::SetImage)); + } + return entry; + }; + + int i = 0; + for (const auto tmp : guild->Emojis) { + const auto emoji = discord.GetEmoji(tmp.ID); + if (!emoji.has_value()) continue; + if (emoji->IsAnimated.has_value() && *emoji->IsAnimated) continue; + if (term.size() > 0) + if (!StringContainsCaseless(emoji->Name, term)) continue; + if (i++ > MaxCompleterEntries) break; + + const auto entry = make_entry(emoji->Name, "<:" + emoji->Name + ":" + std::to_string(emoji->ID) + ">", emoji->GetURL()); + } + + // if <15 guild emojis match then load up stock + if (i < 15) { + auto &emojis = Abaddon::Get().GetEmojis(); + const auto &shortcodes = emojis.GetShortCodes(); + for (const auto &[shortcode, pattern] : shortcodes) { + if (!StringContainsCaseless(shortcode, term)) continue; + if (i++ > 15) break; + const auto &pb = emojis.GetPixBuf(pattern); + if (!pb) continue; + const auto entry = make_entry(shortcode, pattern); + entry->SetImage(pb); + } + } +} + +void Completer::CompleteChannels(const Glib::ustring &term) { + if (!m_channel_id_cb) + return; + + const auto &discord = Abaddon::Get().GetDiscordClient(); + const auto channel_id = m_channel_id_cb(); + const auto channel = discord.GetChannel(channel_id); + if (!channel->GuildID.has_value()) return; + const auto channels = discord.GetChannelsInGuild(*channel->GuildID); + int i = 0; + for (const auto chan_id : channels) { + const auto chan = discord.GetChannel(chan_id); + if (chan->Type == ChannelType::GUILD_VOICE || chan->Type == ChannelType::GUILD_CATEGORY) continue; + if (!StringContainsCaseless(*chan->Name, term)) continue; + if (i++ > MaxCompleterEntries) break; + const auto entry = CreateEntry("<#" + std::to_string(chan_id) + ">"); + entry->SetText("#" + *chan->Name); + } +} + +void Completer::DoCompletion(Gtk::ListBoxRow *row) { + const int index = row->get_index(); + const auto completion = m_entries[index]->GetCompletion(); + const auto it = m_buf->erase(m_start, m_end); // entry is deleted here + m_buf->insert(it, completion + " "); +} + +void Completer::OnRowActivate(Gtk::ListBoxRow *row) { + DoCompletion(row); +} + +void Completer::OnTextBufferChanged() { + const auto term = GetTerm(); + + for (auto it = m_entries.begin(); it != m_entries.end();) { + delete *it; + it = m_entries.erase(it); + } + + switch (term[0]) { + case '@': + CompleteMentions(term.substr(1)); + break; + case ':': + CompleteEmojis(term.substr(1)); + break; + case '#': + CompleteChannels(term.substr(1)); + break; + default: + break; + } + if (m_entries.size() > 0) { + m_list.select_row(*m_entries[0]); + set_reveal_child(true); + } else { + set_reveal_child(false); + } +} + +Glib::ustring Completer::GetTerm() { + const auto iter = m_buf->get_insert()->get_iter(); + Gtk::TextBuffer::iterator dummy; + if (!iter.backward_search(" ", Gtk::TEXT_SEARCH_TEXT_ONLY, m_start, dummy)) + m_buf->get_bounds(m_start, dummy); + else + m_start.forward_char(); // 1 behind + if (!iter.forward_search(" ", Gtk::TEXT_SEARCH_TEXT_ONLY, dummy, m_end)) + m_buf->get_bounds(dummy, m_end); + return m_start.get_text(m_end); +} + +CompleterEntry::CompleterEntry(const Glib::ustring &completion, int index) + : m_index(index) + , m_completion(completion) + , m_box(Gtk::ORIENTATION_HORIZONTAL) { + set_halign(Gtk::ALIGN_START); + get_style_context()->add_class("completer-entry"); + set_can_focus(false); + set_focus_on_click(false); + m_box.show(); + add(m_box); +} + +void CompleterEntry::SetTextColor(int color) { + if (m_text == nullptr) return; + const auto cur = m_text->get_text(); + m_text->set_markup("<span color=\"#" + IntToCSSColor(color) + "\">" + Glib::Markup::escape_text(cur) + "</span>"); +} + +void CompleterEntry::SetText(const Glib::ustring &text) { + if (m_text == nullptr) { + m_text = Gtk::manage(new Gtk::Label); + m_text->get_style_context()->add_class("completer-entry-label"); + m_text->show(); + m_box.pack_end(*m_text); + } + m_text->set_label(text); +} + +void CompleterEntry::SetImage(const Glib::RefPtr<Gdk::Pixbuf> &pb) { + if (m_img == nullptr) { + m_img = Gtk::manage(new Gtk::Image); + m_img->get_style_context()->add_class("completer-entry-image"); + m_img->show(); + m_box.pack_start(*m_img); + } + m_img->property_pixbuf() = pb->scale_simple(24, 24, Gdk::INTERP_BILINEAR); +} + +int CompleterEntry::GetIndex() const { + return m_index; +} + +Glib::ustring CompleterEntry::GetCompletion() const { + return m_completion; +} diff --git a/components/completer.hpp b/components/completer.hpp new file mode 100644 index 0000000..259fcdc --- /dev/null +++ b/components/completer.hpp @@ -0,0 +1,61 @@ +#pragma once +#include <gtkmm.h> +#include <functional> +#include "../discord/snowflake.hpp" + +class CompleterEntry : public Gtk::ListBoxRow { +public: + CompleterEntry(const Glib::ustring &completion, int index); + void SetTextColor(int color); // SetText will reset + void SetText(const Glib::ustring &text); + void SetImage(const Glib::RefPtr<Gdk::Pixbuf> &pb); + + int GetIndex() const; + Glib::ustring GetCompletion() const; + +private: + Glib::ustring m_completion; + int m_index; + Gtk::Box m_box; + Gtk::Label *m_text = nullptr; + Gtk::Image *m_img = nullptr; +}; + +class Completer : public Gtk::Revealer { +public: + Completer(); + Completer(const Glib::RefPtr<Gtk::TextBuffer> &buf); + + void SetBuffer(const Glib::RefPtr<Gtk::TextBuffer> &buf); + bool ProcessKeyPress(GdkEventKey *e); + + using get_recent_authors_cb = std::function<std::vector<Snowflake>()>; + void SetGetRecentAuthors(get_recent_authors_cb cb); // maybe a better way idk + using get_channel_id_cb = std::function<Snowflake()>; + void SetGetChannelID(get_channel_id_cb cb); + + bool IsShown() const; + +private: + CompleterEntry *CreateEntry(const Glib::ustring &completion); + void CompleteMentions(const Glib::ustring &term); + void CompleteEmojis(const Glib::ustring &term); + void CompleteChannels(const Glib::ustring &term); + void DoCompletion(Gtk::ListBoxRow *row); + + std::vector<CompleterEntry *> m_entries; + + void OnRowActivate(Gtk::ListBoxRow *row); + void OnTextBufferChanged(); + Glib::ustring GetTerm(); + + Gtk::TextBuffer::iterator m_start; + Gtk::TextBuffer::iterator m_end; + + Gtk::ScrolledWindow m_scroll; + Gtk::ListBox m_list; + Glib::RefPtr<Gtk::TextBuffer> m_buf; + + get_recent_authors_cb m_recent_authors_cb; + get_channel_id_cb m_channel_id_cb; +}; |