add mention/emoji/channel completion
This commit is contained in:
@@ -52,9 +52,41 @@ ChatWindow::ChatWindow() {
|
|||||||
m_input_scroll->set_max_content_height(200);
|
m_input_scroll->set_max_content_height(200);
|
||||||
m_input_scroll->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
|
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_input_scroll->add(*m_input);
|
||||||
m_scroll->add(*m_list);
|
m_scroll->add(*m_list);
|
||||||
m_main->add(*m_scroll);
|
m_main->add(*m_scroll);
|
||||||
|
m_main->add(m_completer);
|
||||||
m_main->add(*m_input_scroll);
|
m_main->add(*m_input_scroll);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,6 +173,9 @@ Snowflake ChatWindow::GetActiveChannel() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool ChatWindow::on_key_press_event(GdkEventKey *e) {
|
bool ChatWindow::on_key_press_event(GdkEventKey *e) {
|
||||||
|
if (m_completer.ProcessKeyPress(e))
|
||||||
|
return true;
|
||||||
|
|
||||||
if (e->keyval == GDK_KEY_Return) {
|
if (e->keyval == GDK_KEY_Return) {
|
||||||
if (e->state & GDK_SHIFT_MASK)
|
if (e->state & GDK_SHIFT_MASK)
|
||||||
return false;
|
return false;
|
||||||
|
@@ -6,6 +6,7 @@
|
|||||||
#include <set>
|
#include <set>
|
||||||
#include "../discord/discord.hpp"
|
#include "../discord/discord.hpp"
|
||||||
#include "chatmessage.hpp"
|
#include "chatmessage.hpp"
|
||||||
|
#include "completer.hpp"
|
||||||
|
|
||||||
class ChatWindow {
|
class ChatWindow {
|
||||||
public:
|
public:
|
||||||
@@ -65,6 +66,8 @@ protected:
|
|||||||
Gtk::TextView *m_input;
|
Gtk::TextView *m_input;
|
||||||
Gtk::ScrolledWindow *m_input_scroll;
|
Gtk::ScrolledWindow *m_input_scroll;
|
||||||
|
|
||||||
|
Completer m_completer;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
typedef sigc::signal<void, Snowflake, Snowflake> type_signal_action_message_delete;
|
typedef sigc::signal<void, Snowflake, Snowflake> type_signal_action_message_delete;
|
||||||
typedef sigc::signal<void, Snowflake, Snowflake> type_signal_action_message_edit;
|
typedef sigc::signal<void, Snowflake, Snowflake> type_signal_action_message_edit;
|
||||||
|
323
components/completer.cpp
Normal file
323
components/completer.cpp
Normal file
@@ -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;
|
||||||
|
}
|
61
components/completer.hpp
Normal file
61
components/completer.hpp
Normal file
@@ -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;
|
||||||
|
};
|
13
css/main.css
13
css/main.css
@@ -121,6 +121,19 @@
|
|||||||
color: #cfd8dc;
|
color: #cfd8dc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.completer {
|
||||||
|
background-color: #2c3e50;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.completer-entry {
|
||||||
|
color: #cfd8dc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.completer-entry-image {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
paned separator {
|
paned separator {
|
||||||
background: #37474f;
|
background: #37474f;
|
||||||
}
|
}
|
||||||
|
@@ -13,6 +13,10 @@ Snowflake User::GetHoistedRole(Snowflake guild_id, bool with_color) const {
|
|||||||
return Abaddon::Get().GetDiscordClient().GetMemberHoistedRole(guild_id, ID, with_color);
|
return Abaddon::Get().GetDiscordClient().GetMemberHoistedRole(guild_id, ID, with_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string User::GetMention() const {
|
||||||
|
return "<@" + std::to_string(ID) + ">";
|
||||||
|
}
|
||||||
|
|
||||||
void from_json(const nlohmann::json &j, User &m) {
|
void from_json(const nlohmann::json &j, User &m) {
|
||||||
JS_D("id", m.ID);
|
JS_D("id", m.ID);
|
||||||
JS_D("username", m.Username);
|
JS_D("username", m.Username);
|
||||||
|
@@ -31,4 +31,5 @@ struct User {
|
|||||||
bool HasAvatar() const;
|
bool HasAvatar() const;
|
||||||
std::string GetAvatarURL(std::string ext = "png", std::string size = "32") const;
|
std::string GetAvatarURL(std::string ext = "png", std::string size = "32") const;
|
||||||
Snowflake GetHoistedRole(Snowflake guild_id, bool with_color = false) const;
|
Snowflake GetHoistedRole(Snowflake guild_id, bool with_color = false) const;
|
||||||
|
std::string GetMention() const;
|
||||||
};
|
};
|
||||||
|
26
emojis.cpp
26
emojis.cpp
@@ -13,14 +13,24 @@ bool EmojiResource::Load() {
|
|||||||
int num_entries;
|
int num_entries;
|
||||||
std::fread(&num_entries, 4, 1, m_fp);
|
std::fread(&num_entries, 4, 1, m_fp);
|
||||||
for (int i = 0; i < num_entries; i++) {
|
for (int i = 0; i < num_entries; i++) {
|
||||||
static int strsize, len, pos;
|
static int pattern_strlen, shortcode_strlen, len, pos;
|
||||||
std::fread(&strsize, 4, 1, m_fp);
|
std::fread(&pattern_strlen, 4, 1, m_fp);
|
||||||
std::vector<char> str(strsize);
|
std::string pattern(pattern_strlen, '\0');
|
||||||
std::fread(str.data(), strsize, 1, m_fp);
|
std::fread(pattern.data(), pattern_strlen, 1, m_fp);
|
||||||
|
|
||||||
|
const auto pattern_hex = HexToPattern(pattern);
|
||||||
|
|
||||||
|
std::fread(&shortcode_strlen, 4, 1, m_fp);
|
||||||
|
std::string shortcode(shortcode_strlen, '\0');
|
||||||
|
if (shortcode_strlen > 0) {
|
||||||
|
std::fread(shortcode.data(), shortcode_strlen, 1, m_fp);
|
||||||
|
m_shortcode_index[shortcode] = pattern_hex;
|
||||||
|
}
|
||||||
|
|
||||||
std::fread(&len, 4, 1, m_fp);
|
std::fread(&len, 4, 1, m_fp);
|
||||||
std::fread(&pos, 4, 1, m_fp);
|
std::fread(&pos, 4, 1, m_fp);
|
||||||
m_index[std::string(str.begin(), str.end())] = std::make_pair(pos, len);
|
m_index[pattern] = std::make_pair(pos, len);
|
||||||
m_patterns.push_back(HexToPattern(Glib::ustring(str.begin(), str.end())));
|
m_patterns.push_back(pattern_hex);
|
||||||
}
|
}
|
||||||
std::sort(m_patterns.begin(), m_patterns.end(), [](const Glib::ustring &a, const Glib::ustring &b) {
|
std::sort(m_patterns.begin(), m_patterns.end(), [](const Glib::ustring &a, const Glib::ustring &b) {
|
||||||
return a.size() > b.size();
|
return a.size() > b.size();
|
||||||
@@ -113,3 +123,7 @@ void EmojiResource::ReplaceEmojis(Glib::RefPtr<Gtk::TextBuffer> buf, int size) {
|
|||||||
const std::vector<Glib::ustring> &EmojiResource::GetPatterns() const {
|
const std::vector<Glib::ustring> &EmojiResource::GetPatterns() const {
|
||||||
return m_patterns;
|
return m_patterns;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const std::map<std::string, std::string> &EmojiResource::GetShortCodes() const {
|
||||||
|
return m_shortcode_index;
|
||||||
|
}
|
||||||
|
@@ -15,9 +15,11 @@ public:
|
|||||||
static Glib::ustring PatternToHex(const Glib::ustring &pattern);
|
static Glib::ustring PatternToHex(const Glib::ustring &pattern);
|
||||||
static Glib::ustring HexToPattern(Glib::ustring hex);
|
static Glib::ustring HexToPattern(Glib::ustring hex);
|
||||||
const std::vector<Glib::ustring> &GetPatterns() const;
|
const std::vector<Glib::ustring> &GetPatterns() const;
|
||||||
|
const std::map<std::string, std::string> &GetShortCodes() const;
|
||||||
void ReplaceEmojis(Glib::RefPtr<Gtk::TextBuffer> buf, int size = 24);
|
void ReplaceEmojis(Glib::RefPtr<Gtk::TextBuffer> buf, int size = 24);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
std::map<std::string, std::string> m_shortcode_index; // shortcode -> pattern
|
||||||
std::unordered_map<std::string, std::pair<int, int>> m_index; // pattern -> [pos, len]
|
std::unordered_map<std::string, std::pair<int, int>> m_index; // pattern -> [pos, len]
|
||||||
FILE *m_fp = nullptr;
|
FILE *m_fp = nullptr;
|
||||||
std::string m_filepath;
|
std::string m_filepath;
|
||||||
|
BIN
res/emojis.bin
BIN
res/emojis.bin
Binary file not shown.
24
util.cpp
24
util.cpp
@@ -64,6 +64,30 @@ std::string HumanReadableBytes(uint64_t bytes) {
|
|||||||
return std::to_string(bytes) + x[order];
|
return std::to_string(bytes) + x[order];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ScrollListBoxToSelected(Gtk::ListBox &list) {
|
||||||
|
auto cb = [&list]() -> bool {
|
||||||
|
const auto selected = list.get_selected_row();
|
||||||
|
if (selected == nullptr) return false;
|
||||||
|
int x, y;
|
||||||
|
selected->translate_coordinates(list, 0, 0, x, y);
|
||||||
|
if (y < 0) return false;
|
||||||
|
const auto adj = list.get_adjustment();
|
||||||
|
if (!adj) return false;
|
||||||
|
int min, nat;
|
||||||
|
selected->get_preferred_height(min, nat);
|
||||||
|
adj->set_value(y - (adj->get_page_size() - nat) / 2.0);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
Glib::signal_idle().connect(sigc::track_obj(cb, list));
|
||||||
|
}
|
||||||
|
|
||||||
|
// surely theres a better way to do this
|
||||||
|
bool StringContainsCaseless(const Glib::ustring &str, const Glib::ustring &sub) {
|
||||||
|
const auto regex = Glib::Regex::create(Glib::Regex::escape_string(sub), Glib::REGEX_CASELESS);
|
||||||
|
return regex->match(str);
|
||||||
|
}
|
||||||
|
|
||||||
std::string IntToCSSColor(int color) {
|
std::string IntToCSSColor(int color) {
|
||||||
int r = (color & 0xFF0000) >> 16;
|
int r = (color & 0xFF0000) >> 16;
|
||||||
int g = (color & 0x00FF00) >> 8;
|
int g = (color & 0x00FF00) >> 8;
|
||||||
|
4
util.hpp
4
util.hpp
@@ -105,3 +105,7 @@ inline void AlphabeticalSort(T start, T end, std::function<std::string(const typ
|
|||||||
return ac[0] || ac[5];
|
return ac[0] || ac[5];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ScrollListBoxToSelected(Gtk::ListBox &list);
|
||||||
|
|
||||||
|
bool StringContainsCaseless(const Glib::ustring &str, const Glib::ustring &sub);
|
||||||
|
Reference in New Issue
Block a user