summaryrefslogtreecommitdiff
path: root/src/components/channels.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/channels.cpp')
-rw-r--r--src/components/channels.cpp1248
1 files changed, 1248 insertions, 0 deletions
diff --git a/src/components/channels.cpp b/src/components/channels.cpp
new file mode 100644
index 0000000..da31de0
--- /dev/null
+++ b/src/components/channels.cpp
@@ -0,0 +1,1248 @@
+#include "channels.hpp"
+#include <algorithm>
+#include <map>
+#include <unordered_map>
+#include "abaddon.hpp"
+#include "imgmanager.hpp"
+#include "util.hpp"
+#include "statusindicator.hpp"
+
+ChannelList::ChannelList()
+ : Glib::ObjectBase(typeid(ChannelList))
+ , Gtk::ScrolledWindow()
+ , m_model(Gtk::TreeStore::create(m_columns))
+ , m_menu_guild_copy_id("_Copy ID", true)
+ , m_menu_guild_settings("View _Settings", true)
+ , m_menu_guild_leave("_Leave", true)
+ , m_menu_category_copy_id("_Copy ID", true)
+ , m_menu_channel_copy_id("_Copy ID", true)
+ , m_menu_dm_copy_id("_Copy ID", true)
+ , m_menu_dm_close("") // changes depending on if group or not
+ , m_menu_thread_copy_id("_Copy ID", true)
+ , m_menu_thread_leave("_Leave", true)
+ , m_menu_thread_archive("_Archive", true)
+ , m_menu_thread_unarchive("_Unarchive", true) {
+ get_style_context()->add_class("channel-list");
+
+ const auto cb = [this](const Gtk::TreeModel::Path &path, Gtk::TreeViewColumn *column) {
+ auto row = *m_model->get_iter(path);
+ const auto type = row[m_columns.m_type];
+ // text channels should not be allowed to be collapsed
+ // maybe they should be but it seems a little difficult to handle expansion to permit this
+ if (type != RenderType::TextChannel) {
+ if (row[m_columns.m_expanded]) {
+ m_view.collapse_row(path);
+ row[m_columns.m_expanded] = false;
+ } else {
+ m_view.expand_row(path, false);
+ row[m_columns.m_expanded] = true;
+ }
+ }
+
+ if (type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread) {
+ m_signal_action_channel_item_select.emit(static_cast<Snowflake>(row[m_columns.m_id]));
+ }
+ };
+ m_view.signal_row_activated().connect(cb, false);
+ m_view.signal_row_collapsed().connect(sigc::mem_fun(*this, &ChannelList::OnRowCollapsed), false);
+ m_view.signal_row_expanded().connect(sigc::mem_fun(*this, &ChannelList::OnRowExpanded), false);
+ m_view.set_activate_on_single_click(true);
+ m_view.get_selection()->set_mode(Gtk::SELECTION_SINGLE);
+ m_view.get_selection()->set_select_function(sigc::mem_fun(*this, &ChannelList::SelectionFunc));
+ m_view.signal_button_press_event().connect(sigc::mem_fun(*this, &ChannelList::OnButtonPressEvent), false);
+
+ m_view.set_hexpand(true);
+ m_view.set_vexpand(true);
+
+ m_view.set_show_expanders(false);
+ m_view.set_enable_search(false);
+ m_view.set_headers_visible(false);
+ m_view.set_model(m_model);
+ m_model->set_sort_column(m_columns.m_sort, Gtk::SORT_ASCENDING);
+
+ m_model->signal_row_inserted().connect([this](const Gtk::TreeModel::Path &path, const Gtk::TreeModel::iterator &iter) {
+ if (m_updating_listing) return;
+ if (auto parent = iter->parent(); parent && (*parent)[m_columns.m_expanded])
+ m_view.expand_row(m_model->get_path(parent), false);
+ });
+
+ m_view.show();
+
+ add(m_view);
+
+ auto *column = Gtk::manage(new Gtk::TreeView::Column("display"));
+ auto *renderer = Gtk::manage(new CellRendererChannels);
+ column->pack_start(*renderer);
+ column->add_attribute(renderer->property_type(), m_columns.m_type);
+ column->add_attribute(renderer->property_icon(), m_columns.m_icon);
+ column->add_attribute(renderer->property_icon_animation(), m_columns.m_icon_anim);
+ column->add_attribute(renderer->property_name(), m_columns.m_name);
+ column->add_attribute(renderer->property_expanded(), m_columns.m_expanded);
+ column->add_attribute(renderer->property_nsfw(), m_columns.m_nsfw);
+ m_view.append_column(*column);
+
+ m_menu_guild_copy_id.signal_activate().connect([this] {
+ Gtk::Clipboard::get()->set_text(std::to_string((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]));
+ });
+ m_menu_guild_settings.signal_activate().connect([this] {
+ m_signal_action_guild_settings.emit(static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]));
+ });
+ m_menu_guild_leave.signal_activate().connect([this] {
+ m_signal_action_guild_leave.emit(static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]));
+ });
+ m_menu_guild.append(m_menu_guild_copy_id);
+ m_menu_guild.append(m_menu_guild_settings);
+ m_menu_guild.append(m_menu_guild_leave);
+ m_menu_guild.show_all();
+
+ m_menu_category_copy_id.signal_activate().connect([this] {
+ Gtk::Clipboard::get()->set_text(std::to_string((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]));
+ });
+ m_menu_category.append(m_menu_category_copy_id);
+ m_menu_category.show_all();
+
+ m_menu_channel_copy_id.signal_activate().connect([this] {
+ Gtk::Clipboard::get()->set_text(std::to_string((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]));
+ });
+ m_menu_channel.append(m_menu_channel_copy_id);
+ m_menu_channel.show_all();
+
+ m_menu_dm_copy_id.signal_activate().connect([this] {
+ Gtk::Clipboard::get()->set_text(std::to_string((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]));
+ });
+ m_menu_dm_close.signal_activate().connect([this] {
+ const auto id = static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]);
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ const auto channel = discord.GetChannel(id);
+ if (!channel.has_value()) return;
+
+ if (channel->Type == ChannelType::DM)
+ discord.CloseDM(id);
+ else if (Abaddon::Get().ShowConfirm("Are you sure you want to leave this group DM?"))
+ Abaddon::Get().GetDiscordClient().CloseDM(id);
+ });
+ m_menu_dm.append(m_menu_dm_copy_id);
+ m_menu_dm.append(m_menu_dm_close);
+ m_menu_dm.show_all();
+
+ m_menu_thread_copy_id.signal_activate().connect([this] {
+ Gtk::Clipboard::get()->set_text(std::to_string((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]));
+ });
+ m_menu_thread_leave.signal_activate().connect([this] {
+ if (Abaddon::Get().ShowConfirm("Are you sure you want to leave this thread?"))
+ Abaddon::Get().GetDiscordClient().LeaveThread(static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]), "Context%20Menu", [](...) {});
+ });
+ m_menu_thread_archive.signal_activate().connect([this] {
+ Abaddon::Get().GetDiscordClient().ArchiveThread(static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]), [](...) {});
+ });
+ m_menu_thread_unarchive.signal_activate().connect([this] {
+ Abaddon::Get().GetDiscordClient().UnArchiveThread(static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]), [](...) {});
+ });
+ m_menu_thread.append(m_menu_thread_copy_id);
+ m_menu_thread.append(m_menu_thread_leave);
+ m_menu_thread.append(m_menu_thread_archive);
+ m_menu_thread.append(m_menu_thread_unarchive);
+ m_menu_thread.show_all();
+
+ m_menu_thread.signal_popped_up().connect(sigc::mem_fun(*this, &ChannelList::OnThreadSubmenuPopup));
+
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ discord.signal_message_create().connect(sigc::mem_fun(*this, &ChannelList::OnMessageCreate));
+ discord.signal_guild_create().connect(sigc::mem_fun(*this, &ChannelList::UpdateNewGuild));
+ discord.signal_guild_delete().connect(sigc::mem_fun(*this, &ChannelList::UpdateRemoveGuild));
+ discord.signal_channel_delete().connect(sigc::mem_fun(*this, &ChannelList::UpdateRemoveChannel));
+ discord.signal_channel_update().connect(sigc::mem_fun(*this, &ChannelList::UpdateChannel));
+ discord.signal_channel_create().connect(sigc::mem_fun(*this, &ChannelList::UpdateCreateChannel));
+ discord.signal_thread_delete().connect(sigc::mem_fun(*this, &ChannelList::OnThreadDelete));
+ discord.signal_thread_update().connect(sigc::mem_fun(*this, &ChannelList::OnThreadUpdate));
+ discord.signal_thread_list_sync().connect(sigc::mem_fun(*this, &ChannelList::OnThreadListSync));
+ discord.signal_added_to_thread().connect(sigc::mem_fun(*this, &ChannelList::OnThreadJoined));
+ discord.signal_removed_from_thread().connect(sigc::mem_fun(*this, &ChannelList::OnThreadRemoved));
+ discord.signal_guild_update().connect(sigc::mem_fun(*this, &ChannelList::UpdateGuild));
+}
+
+void ChannelList::UpdateListing() {
+ m_updating_listing = true;
+
+ m_model->clear();
+
+ auto &discord = Abaddon::Get().GetDiscordClient();
+
+ const auto guild_ids = discord.GetUserSortedGuilds();
+ int sortnum = 0;
+ for (const auto &guild_id : guild_ids) {
+ const auto guild = discord.GetGuild(guild_id);
+ if (!guild.has_value()) continue;
+
+ auto iter = AddGuild(*guild);
+ (*iter)[m_columns.m_sort] = sortnum++;
+ }
+
+ m_updating_listing = false;
+
+ AddPrivateChannels();
+}
+
+void ChannelList::UpdateNewGuild(const GuildData &guild) {
+ AddGuild(guild);
+ // update sort order
+ int sortnum = 0;
+ for (const auto guild_id : Abaddon::Get().GetDiscordClient().GetUserSortedGuilds()) {
+ auto iter = GetIteratorForGuildFromID(guild_id);
+ if (iter)
+ (*iter)[m_columns.m_sort] = ++sortnum;
+ }
+}
+
+void ChannelList::UpdateRemoveGuild(Snowflake id) {
+ auto iter = GetIteratorForGuildFromID(id);
+ if (!iter) return;
+ m_model->erase(iter);
+}
+
+void ChannelList::UpdateRemoveChannel(Snowflake id) {
+ auto iter = GetIteratorForChannelFromID(id);
+ if (!iter) return;
+ m_model->erase(iter);
+}
+
+void ChannelList::UpdateChannel(Snowflake id) {
+ auto iter = GetIteratorForChannelFromID(id);
+ auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id);
+ if (!iter || !channel.has_value()) return;
+ if (channel->Type == ChannelType::GUILD_CATEGORY) return UpdateChannelCategory(*channel);
+ if (!IsTextChannel(channel->Type)) return;
+
+ // refresh stuff that might have changed
+ const bool is_orphan_TMP = !channel->ParentID.has_value();
+ (*iter)[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel->Name);
+ (*iter)[m_columns.m_nsfw] = channel->NSFW();
+ (*iter)[m_columns.m_sort] = *channel->Position + (is_orphan_TMP ? OrphanChannelSortOffset : 0);
+
+ // check if the parent has changed
+ Gtk::TreeModel::iterator new_parent;
+ if (channel->ParentID.has_value())
+ new_parent = GetIteratorForChannelFromID(*channel->ParentID);
+ else if (channel->GuildID.has_value())
+ new_parent = GetIteratorForGuildFromID(*channel->GuildID);
+
+ if (new_parent && iter->parent() != new_parent)
+ MoveRow(iter, new_parent);
+}
+
+void ChannelList::UpdateCreateChannel(const ChannelData &channel) {
+ ;
+ if (channel.Type == ChannelType::GUILD_CATEGORY) return (void)UpdateCreateChannelCategory(channel);
+ if (channel.Type == ChannelType::DM || channel.Type == ChannelType::GROUP_DM) return UpdateCreateDMChannel(channel);
+ if (channel.Type != ChannelType::GUILD_TEXT && channel.Type != ChannelType::GUILD_NEWS) return;
+
+ Gtk::TreeRow channel_row;
+ bool orphan;
+ if (channel.ParentID.has_value()) {
+ orphan = false;
+ auto iter = GetIteratorForChannelFromID(*channel.ParentID);
+ channel_row = *m_model->append(iter->children());
+ } else {
+ orphan = true;
+ auto iter = GetIteratorForGuildFromID(*channel.GuildID);
+ channel_row = *m_model->append(iter->children());
+ }
+ channel_row[m_columns.m_type] = RenderType::TextChannel;
+ channel_row[m_columns.m_id] = channel.ID;
+ channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name);
+ channel_row[m_columns.m_nsfw] = channel.NSFW();
+ if (orphan)
+ channel_row[m_columns.m_sort] = *channel.Position + OrphanChannelSortOffset;
+ else
+ channel_row[m_columns.m_sort] = *channel.Position;
+}
+
+void ChannelList::UpdateGuild(Snowflake id) {
+ auto iter = GetIteratorForGuildFromID(id);
+ auto &img = Abaddon::Get().GetImageManager();
+ const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(id);
+ if (!iter || !guild.has_value()) return;
+
+ static const bool show_animations = Abaddon::Get().GetSettings().GetShowAnimations();
+
+ (*iter)[m_columns.m_name] = "<b>" + Glib::Markup::escape_text(guild->Name) + "</b>";
+ (*iter)[m_columns.m_icon] = img.GetPlaceholder(GuildIconSize);
+ if (show_animations && guild->HasAnimatedIcon()) {
+ const auto cb = [this, id](const Glib::RefPtr<Gdk::PixbufAnimation> &pb) {
+ auto iter = GetIteratorForGuildFromID(id);
+ if (iter) (*iter)[m_columns.m_icon_anim] = pb;
+ };
+ img.LoadAnimationFromURL(guild->GetIconURL("gif", "32"), GuildIconSize, GuildIconSize, sigc::track_obj(cb, *this));
+ } else if (guild->HasIcon()) {
+ const auto cb = [this, id](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
+ // iter might be invalid
+ auto iter = GetIteratorForGuildFromID(id);
+ if (iter) (*iter)[m_columns.m_icon] = pb->scale_simple(GuildIconSize, GuildIconSize, Gdk::INTERP_BILINEAR);
+ };
+ img.LoadFromURL(guild->GetIconURL("png", "32"), sigc::track_obj(cb, *this));
+ }
+}
+
+void ChannelList::OnThreadJoined(Snowflake id) {
+ if (GetIteratorForChannelFromID(id)) return;
+ const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id);
+ if (!channel.has_value()) return;
+ const auto parent = GetIteratorForChannelFromID(*channel->ParentID);
+ if (parent)
+ CreateThreadRow(parent->children(), *channel);
+}
+
+void ChannelList::OnThreadRemoved(Snowflake id) {
+ DeleteThreadRow(id);
+}
+
+void ChannelList::OnThreadDelete(const ThreadDeleteData &data) {
+ DeleteThreadRow(data.ID);
+}
+
+// todo probably make the row stick around if its selected until the selection changes
+void ChannelList::OnThreadUpdate(const ThreadUpdateData &data) {
+ auto iter = GetIteratorForChannelFromID(data.Thread.ID);
+ if (iter)
+ (*iter)[m_columns.m_name] = "- " + Glib::Markup::escape_text(*data.Thread.Name);
+
+ if (data.Thread.ThreadMetadata->IsArchived)
+ DeleteThreadRow(data.Thread.ID);
+}
+
+void ChannelList::OnThreadListSync(const ThreadListSyncData &data) {
+ // get the threads in the guild
+ std::vector<Snowflake> threads;
+ auto guild_iter = GetIteratorForGuildFromID(data.GuildID);
+ std::queue<Gtk::TreeModel::iterator> queue;
+ queue.push(guild_iter);
+
+ while (!queue.empty()) {
+ auto item = queue.front();
+ queue.pop();
+ if ((*item)[m_columns.m_type] == RenderType::Thread)
+ threads.push_back(static_cast<Snowflake>((*item)[m_columns.m_id]));
+ for (auto child : item->children())
+ queue.push(child);
+ }
+
+ // delete all threads not present in the synced data
+ for (auto thread_id : threads) {
+ if (std::find_if(data.Threads.begin(), data.Threads.end(), [thread_id](const auto &x) { return x.ID == thread_id; }) == data.Threads.end()) {
+ auto iter = GetIteratorForChannelFromID(thread_id);
+ m_model->erase(iter);
+ }
+ }
+
+ // delete all archived threads
+ for (auto thread : data.Threads) {
+ if (thread.ThreadMetadata->IsArchived) {
+ if (auto iter = GetIteratorForChannelFromID(thread.ID))
+ m_model->erase(iter);
+ }
+ }
+}
+
+void ChannelList::DeleteThreadRow(Snowflake id) {
+ auto iter = GetIteratorForChannelFromID(id);
+ if (iter)
+ m_model->erase(iter);
+}
+
+// create a temporary channel row for non-joined threads
+// and delete them when the active channel switches off of them if still not joined
+void ChannelList::SetActiveChannel(Snowflake id) {
+ if (m_temporary_thread_row) {
+ const auto thread_id = static_cast<Snowflake>((*m_temporary_thread_row)[m_columns.m_id]);
+ const auto thread = Abaddon::Get().GetDiscordClient().GetChannel(thread_id);
+ if (thread.has_value() && (!thread->IsJoinedThread() || thread->ThreadMetadata->IsArchived))
+ m_model->erase(m_temporary_thread_row);
+ m_temporary_thread_row = {};
+ }
+
+ const auto channel_iter = GetIteratorForChannelFromID(id);
+ if (channel_iter) {
+ m_view.expand_to_path(m_model->get_path(channel_iter));
+ m_view.get_selection()->select(channel_iter);
+ } else {
+ m_view.get_selection()->unselect_all();
+ // SetActiveChannel should probably just take the channel object
+ const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id);
+ if (!channel.has_value() || !channel->IsThread()) return;
+ auto parent_iter = GetIteratorForChannelFromID(*channel->ParentID);
+ if (!parent_iter) return;
+ m_temporary_thread_row = CreateThreadRow(parent_iter->children(), *channel);
+ m_view.get_selection()->select(m_temporary_thread_row);
+ }
+}
+
+void ChannelList::UseExpansionState(const ExpansionStateRoot &root) {
+ auto recurse = [this](auto &self, const ExpansionStateRoot &root) -> void {
+ // and these are only channels
+ for (const auto &[id, state] : root.Children) {
+ if (const auto iter = GetIteratorForChannelFromID(id)) {
+ if (state.IsExpanded)
+ m_view.expand_row(m_model->get_path(iter), false);
+ else
+ m_view.collapse_row(m_model->get_path(iter));
+ }
+
+ self(self, state.Children);
+ }
+ };
+
+ // top level is guild
+ for (const auto &[id, state] : root.Children) {
+ if (const auto iter = GetIteratorForGuildFromID(id)) {
+ if (state.IsExpanded)
+ m_view.expand_row(m_model->get_path(iter), false);
+ else
+ m_view.collapse_row(m_model->get_path(iter));
+ }
+
+ recurse(recurse, state.Children);
+ }
+}
+
+ExpansionStateRoot ChannelList::GetExpansionState() const {
+ ExpansionStateRoot r;
+
+ auto recurse = [this](auto &self, const Gtk::TreeRow &row) -> ExpansionState {
+ ExpansionState r;
+
+ r.IsExpanded = row[m_columns.m_expanded];
+ for (const auto &child : row.children())
+ r.Children.Children[static_cast<Snowflake>(child[m_columns.m_id])] = self(self, child);
+
+ return r;
+ };
+
+ for (const auto &child : m_model->children()) {
+ const auto id = static_cast<Snowflake>(child[m_columns.m_id]);
+ if (static_cast<uint64_t>(id) == 0ULL) continue; // dont save DM header
+ r.Children[id] = recurse(recurse, child);
+ }
+
+ return r;
+}
+
+Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) {
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ auto &img = Abaddon::Get().GetImageManager();
+
+ auto guild_row = *m_model->append();
+ guild_row[m_columns.m_type] = RenderType::Guild;
+ guild_row[m_columns.m_id] = guild.ID;
+ guild_row[m_columns.m_name] = "<b>" + Glib::Markup::escape_text(guild.Name) + "</b>";
+ guild_row[m_columns.m_icon] = img.GetPlaceholder(GuildIconSize);
+
+ static const bool show_animations = Abaddon::Get().GetSettings().GetShowAnimations();
+
+ if (show_animations && guild.HasAnimatedIcon()) {
+ const auto cb = [this, id = guild.ID](const Glib::RefPtr<Gdk::PixbufAnimation> &pb) {
+ auto iter = GetIteratorForGuildFromID(id);
+ if (iter) (*iter)[m_columns.m_icon_anim] = pb;
+ };
+ img.LoadAnimationFromURL(guild.GetIconURL("gif", "32"), GuildIconSize, GuildIconSize, sigc::track_obj(cb, *this));
+ } else if (guild.HasIcon()) {
+ const auto cb = [this, id = guild.ID](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
+ auto iter = GetIteratorForGuildFromID(id);
+ if (iter) (*iter)[m_columns.m_icon] = pb->scale_simple(GuildIconSize, GuildIconSize, Gdk::INTERP_BILINEAR);
+ };
+ img.LoadFromURL(guild.GetIconURL("png", "32"), sigc::track_obj(cb, *this));
+ }
+
+ if (!guild.Channels.has_value()) return guild_row;
+
+ // separate out the channels
+ std::vector<ChannelData> orphan_channels;
+ std::map<Snowflake, std::vector<ChannelData>> categories;
+
+ for (const auto &channel_ : *guild.Channels) {
+ const auto channel = discord.GetChannel(channel_.ID);
+ if (!channel.has_value()) continue;
+ if (channel->Type == ChannelType::GUILD_TEXT || channel->Type == ChannelType::GUILD_NEWS) {
+ if (channel->ParentID.has_value())
+ categories[*channel->ParentID].push_back(*channel);
+ else
+ orphan_channels.push_back(*channel);
+ } else if (channel->Type == ChannelType::GUILD_CATEGORY) {
+ categories[channel->ID];
+ }
+ }
+
+ std::map<Snowflake, std::vector<ChannelData>> threads;
+ for (const auto &tmp : *guild.Threads) {
+ const auto thread = discord.GetChannel(tmp.ID);
+ if (thread.has_value())
+ threads[*thread->ParentID].push_back(*thread);
+ }
+ const auto add_threads = [&](const ChannelData &channel, Gtk::TreeRow row) {
+ row[m_columns.m_expanded] = true;
+
+ const auto it = threads.find(channel.ID);
+ if (it == threads.end()) return;
+
+ for (const auto &thread : it->second)
+ CreateThreadRow(row.children(), thread);
+ };
+
+ for (const auto &channel : orphan_channels) {
+ auto channel_row = *m_model->append(guild_row.children());
+ channel_row[m_columns.m_type] = RenderType::TextChannel;
+ channel_row[m_columns.m_id] = channel.ID;
+ channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name);
+ channel_row[m_columns.m_sort] = *channel.Position + OrphanChannelSortOffset;
+ channel_row[m_columns.m_nsfw] = channel.NSFW();
+ add_threads(channel, channel_row);
+ }
+
+ for (const auto &[category_id, channels] : categories) {
+ const auto category = discord.GetChannel(category_id);
+ if (!category.has_value()) continue;
+ auto cat_row = *m_model->append(guild_row.children());
+ cat_row[m_columns.m_type] = RenderType::Category;
+ cat_row[m_columns.m_id] = category_id;
+ cat_row[m_columns.m_name] = Glib::Markup::escape_text(*category->Name);
+ cat_row[m_columns.m_sort] = *category->Position;
+ cat_row[m_columns.m_expanded] = true;
+ // m_view.expand_row wont work because it might not have channels
+
+ for (const auto &channel : channels) {
+ auto channel_row = *m_model->append(cat_row.children());
+ channel_row[m_columns.m_type] = RenderType::TextChannel;
+ channel_row[m_columns.m_id] = channel.ID;
+ channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name);
+ channel_row[m_columns.m_sort] = *channel.Position;
+ channel_row[m_columns.m_nsfw] = channel.NSFW();
+ add_threads(channel, channel_row);
+ }
+ }
+
+ return guild_row;
+}
+
+Gtk::TreeModel::iterator ChannelList::UpdateCreateChannelCategory(const ChannelData &channel) {
+ const auto iter = GetIteratorForGuildFromID(*channel.GuildID);
+ if (!iter) return {};
+
+ auto cat_row = *m_model->append(iter->children());
+ cat_row[m_columns.m_type] = RenderType::Category;
+ cat_row[m_columns.m_id] = channel.ID;
+ cat_row[m_columns.m_name] = Glib::Markup::escape_text(*channel.Name);
+ cat_row[m_columns.m_sort] = *channel.Position;
+ cat_row[m_columns.m_expanded] = true;
+
+ return cat_row;
+}
+
+Gtk::TreeModel::iterator ChannelList::CreateThreadRow(const Gtk::TreeNodeChildren &children, const ChannelData &channel) {
+ auto thread_iter = m_model->append(children);
+ auto thread_row = *thread_iter;
+ thread_row[m_columns.m_type] = RenderType::Thread;
+ thread_row[m_columns.m_id] = channel.ID;
+ thread_row[m_columns.m_name] = "- " + Glib::Markup::escape_text(*channel.Name);
+ thread_row[m_columns.m_sort] = channel.ID;
+ thread_row[m_columns.m_nsfw] = false;
+
+ return thread_iter;
+}
+
+void ChannelList::UpdateChannelCategory(const ChannelData &channel) {
+ auto iter = GetIteratorForChannelFromID(channel.ID);
+ if (!iter) return;
+
+ (*iter)[m_columns.m_sort] = *channel.Position;
+ (*iter)[m_columns.m_name] = Glib::Markup::escape_text(*channel.Name);
+}
+
+Gtk::TreeModel::iterator ChannelList::GetIteratorForGuildFromID(Snowflake id) {
+ for (const auto &child : m_model->children()) {
+ if (child[m_columns.m_id] == id)
+ return child;
+ }
+ return {};
+}
+
+Gtk::TreeModel::iterator ChannelList::GetIteratorForChannelFromID(Snowflake id) {
+ std::queue<Gtk::TreeModel::iterator> queue;
+ for (const auto &child : m_model->children())
+ for (const auto &child2 : child.children())
+ queue.push(child2);
+
+ while (!queue.empty()) {
+ auto item = queue.front();
+ if ((*item)[m_columns.m_id] == id) return item;
+ for (const auto &child : item->children())
+ queue.push(child);
+ queue.pop();
+ }
+
+ return {};
+}
+
+bool ChannelList::IsTextChannel(ChannelType type) {
+ return type == ChannelType::GUILD_TEXT || type == ChannelType::GUILD_NEWS;
+}
+
+// this should be unncessary but something is behaving strange so its just in case
+void ChannelList::OnRowCollapsed(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path) {
+ (*iter)[m_columns.m_expanded] = false;
+}
+
+void ChannelList::OnRowExpanded(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path) {
+ // restore previous expansion
+ for (auto it = iter->children().begin(); it != iter->children().end(); it++) {
+ if ((*it)[m_columns.m_expanded])
+ m_view.expand_row(m_model->get_path(it), false);
+ }
+
+ // try and restore selection if previous collapsed
+ if (auto selection = m_view.get_selection(); selection && !selection->get_selected()) {
+ selection->select(m_last_selected);
+ }
+
+ (*iter)[m_columns.m_expanded] = true;
+}
+
+bool ChannelList::SelectionFunc(const Glib::RefPtr<Gtk::TreeModel> &model, const Gtk::TreeModel::Path &path, bool is_currently_selected) {
+ if (auto selection = m_view.get_selection())
+ if (auto row = selection->get_selected())
+ m_last_selected = m_model->get_path(row);
+
+ auto type = (*m_model->get_iter(path))[m_columns.m_type];
+ return type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread;
+}
+
+void ChannelList::AddPrivateChannels() {
+ auto header_row = *m_model->append();
+ header_row[m_columns.m_type] = RenderType::DMHeader;
+ header_row[m_columns.m_sort] = -1;
+ header_row[m_columns.m_name] = "<b>Direct Messages</b>";
+ m_dm_header = m_model->get_path(header_row);
+
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ auto &img = Abaddon::Get().GetImageManager();
+
+ const auto dm_ids = discord.GetPrivateChannels();
+ for (const auto dm_id : dm_ids) {
+ const auto dm = discord.GetChannel(dm_id);
+ if (!dm.has_value()) continue;
+
+ std::optional<UserData> top_recipient;
+ const auto recipients = dm->GetDMRecipients();
+ if (recipients.size() > 0)
+ top_recipient = recipients[0];
+
+ auto iter = m_model->append(header_row->children());
+ auto row = *iter;
+ row[m_columns.m_type] = RenderType::DM;
+ row[m_columns.m_id] = dm_id;
+ row[m_columns.m_sort] = -(dm->LastMessageID.has_value() ? *dm->LastMessageID : dm_id);
+ row[m_columns.m_icon] = img.GetPlaceholder(DMIconSize);
+
+ if (dm->Type == ChannelType::DM && top_recipient.has_value())
+ row[m_columns.m_name] = Glib::Markup::escape_text(top_recipient->Username);
+ else if (dm->Type == ChannelType::GROUP_DM)
+ row[m_columns.m_name] = std::to_string(recipients.size()) + " members";
+
+ if (top_recipient.has_value()) {
+ const auto cb = [this, iter](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
+ if (iter)
+ (*iter)[m_columns.m_icon] = pb->scale_simple(DMIconSize, DMIconSize, Gdk::INTERP_BILINEAR);
+ };
+ img.LoadFromURL(top_recipient->GetAvatarURL("png", "32"), sigc::track_obj(cb, *this));
+ }
+ }
+}
+
+void ChannelList::UpdateCreateDMChannel(const ChannelData &dm) {
+ auto header_row = m_model->get_iter(m_dm_header);
+ auto &img = Abaddon::Get().GetImageManager();
+
+ std::optional<UserData> top_recipient;
+ const auto recipients = dm.GetDMRecipients();
+ if (recipients.size() > 0)
+ top_recipient = recipients[0];
+
+ auto iter = m_model->append(header_row->children());
+ auto row = *iter;
+ row[m_columns.m_type] = RenderType::DM;
+ row[m_columns.m_id] = dm.ID;
+ row[m_columns.m_sort] = -(dm.LastMessageID.has_value() ? *dm.LastMessageID : dm.ID);
+ row[m_columns.m_icon] = img.GetPlaceholder(DMIconSize);
+
+ if (dm.Type == ChannelType::DM && top_recipient.has_value())
+ row[m_columns.m_name] = Glib::Markup::escape_text(top_recipient->Username);
+ else if (dm.Type == ChannelType::GROUP_DM)
+ row[m_columns.m_name] = std::to_string(recipients.size()) + " members";
+
+ if (top_recipient.has_value()) {
+ const auto cb = [this, iter](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
+ if (iter)
+ (*iter)[m_columns.m_icon] = pb->scale_simple(DMIconSize, DMIconSize, Gdk::INTERP_BILINEAR);
+ };
+ img.LoadFromURL(top_recipient->GetAvatarURL("png", "32"), sigc::track_obj(cb, *this));
+ }
+}
+
+void ChannelList::OnMessageCreate(const Message &msg) {
+ const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(msg.ChannelID);
+ if (!channel.has_value()) return;
+ if (channel->Type != ChannelType::DM && channel->Type != ChannelType::GROUP_DM) return;
+ auto iter = GetIteratorForChannelFromID(msg.ChannelID);
+ if (iter)
+ (*iter)[m_columns.m_sort] = -msg.ID;
+}
+
+bool ChannelList::OnButtonPressEvent(GdkEventButton *ev) {
+ if (ev->button == GDK_BUTTON_SECONDARY && ev->type == GDK_BUTTON_PRESS) {
+ if (m_view.get_path_at_pos(ev->x, ev->y, m_path_for_menu)) {
+ auto row = (*m_model->get_iter(m_path_for_menu));
+ switch (static_cast<RenderType>(row[m_columns.m_type])) {
+ case RenderType::Guild:
+ m_menu_guild.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
+ break;
+ case RenderType::Category:
+ m_menu_category.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
+ break;
+ case RenderType::TextChannel:
+ m_menu_channel.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
+ break;
+ case RenderType::DM: {
+ const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(static_cast<Snowflake>(row[m_columns.m_id]));
+ if (channel.has_value()) {
+ m_menu_dm_close.set_label(channel->Type == ChannelType::DM ? "Close" : "Leave");
+ m_menu_dm_close.show();
+ } else
+ m_menu_dm_close.hide();
+ m_menu_dm.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
+ } break;
+ case RenderType::Thread: {
+ m_menu_thread.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
+ break;
+ } break;
+ default:
+ break;
+ }
+ }
+ return true;
+ }
+ return false;
+}
+
+void ChannelList::MoveRow(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::iterator &new_parent) {
+ // duplicate the row data under the new parent and then delete the old row
+ auto row = *m_model->append(new_parent->children());
+ // would be nice to be able to get all columns out at runtime so i dont need this
+#define M(name) \
+ row[m_columns.name] = static_cast<decltype(m_columns.name)::ElementType>((*iter)[m_columns.name]);
+ M(m_type);
+ M(m_id);
+ M(m_name);
+ M(m_icon);
+ M(m_icon_anim);
+ M(m_sort);
+ M(m_nsfw);
+ M(m_expanded);
+#undef M
+
+ // recursively move children
+ // weird construct to work around iterator invalidation (at least i think thats what the problem was)
+ const auto tmp = iter->children();
+ const auto children = std::vector<Gtk::TreeRow>(tmp.begin(), tmp.end());
+ for (size_t i = 0; i < children.size(); i++)
+ MoveRow(children[i], row);
+
+ // delete original
+ m_model->erase(iter);
+}
+
+void ChannelList::OnThreadSubmenuPopup(const Gdk::Rectangle *flipped_rect, const Gdk::Rectangle *final_rect, bool flipped_x, bool flipped_y) {
+ m_menu_thread_archive.set_visible(false);
+ m_menu_thread_unarchive.set_visible(false);
+
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ auto iter = m_model->get_iter(m_path_for_menu);
+ if (!iter) return;
+ auto channel = discord.GetChannel(static_cast<Snowflake>((*iter)[m_columns.m_id]));
+ if (!channel.has_value() || !channel->ThreadMetadata.has_value()) return;
+ if (!discord.HasGuildPermission(discord.GetUserData().ID, *channel->GuildID, Permission::MANAGE_THREADS)) return;
+
+ m_menu_thread_archive.set_visible(!channel->ThreadMetadata->IsArchived);
+ m_menu_thread_unarchive.set_visible(channel->ThreadMetadata->IsArchived);
+}
+
+ChannelList::type_signal_action_channel_item_select ChannelList::signal_action_channel_item_select() {
+ return m_signal_action_channel_item_select;
+}
+
+ChannelList::type_signal_action_guild_leave ChannelList::signal_action_guild_leave() {
+ return m_signal_action_guild_leave;
+}
+
+ChannelList::type_signal_action_guild_settings ChannelList::signal_action_guild_settings() {
+ return m_signal_action_guild_settings;
+}
+
+ChannelList::ModelColumns::ModelColumns() {
+ add(m_type);
+ add(m_id);
+ add(m_name);
+ add(m_icon);
+ add(m_icon_anim);
+ add(m_sort);
+ add(m_nsfw);
+ add(m_expanded);
+}
+
+CellRendererChannels::CellRendererChannels()
+ : Glib::ObjectBase(typeid(CellRendererChannels))
+ , Gtk::CellRenderer()
+ , m_property_type(*this, "render-type")
+ , m_property_name(*this, "name")
+ , m_property_pixbuf(*this, "pixbuf")
+ , m_property_pixbuf_animation(*this, "pixbuf-animation")
+ , m_property_expanded(*this, "expanded")
+ , m_property_nsfw(*this, "nsfw") {
+ property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE;
+ property_xpad() = 2;
+ property_ypad() = 2;
+ m_property_name.get_proxy().signal_changed().connect([this] {
+ m_renderer_text.property_markup() = m_property_name;
+ });
+}
+
+CellRendererChannels::~CellRendererChannels() {
+}
+
+Glib::PropertyProxy<RenderType> CellRendererChannels::property_type() {
+ return m_property_type.get_proxy();
+}
+
+Glib::PropertyProxy<Glib::ustring> CellRendererChannels::property_name() {
+ return m_property_name.get_proxy();
+}
+
+Glib::PropertyProxy<Glib::RefPtr<Gdk::Pixbuf>> CellRendererChannels::property_icon() {
+ return m_property_pixbuf.get_proxy();
+}
+
+Glib::PropertyProxy<Glib::RefPtr<Gdk::PixbufAnimation>> CellRendererChannels::property_icon_animation() {
+ return m_property_pixbuf_animation.get_proxy();
+}
+
+Glib::PropertyProxy<bool> CellRendererChannels::property_expanded() {
+ return m_property_expanded.get_proxy();
+}
+
+Glib::PropertyProxy<bool> CellRendererChannels::property_nsfw() {
+ return m_property_nsfw.get_proxy();
+}
+
+void CellRendererChannels::get_preferred_width_vfunc(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
+ switch (m_property_type.get_value()) {
+ case RenderType::Guild:
+ return get_preferred_width_vfunc_guild(widget, minimum_width, natural_width);
+ case RenderType::Category:
+ return get_preferred_width_vfunc_category(widget, minimum_width, natural_width);
+ case RenderType::TextChannel:
+ return get_preferred_width_vfunc_channel(widget, minimum_width, natural_width);
+ case RenderType::Thread:
+ return get_preferred_width_vfunc_thread(widget, minimum_width, natural_width);
+ case RenderType::DMHeader:
+ return get_preferred_width_vfunc_dmheader(widget, minimum_width, natural_width);
+ case RenderType::DM:
+ return get_preferred_width_vfunc_dm(widget, minimum_width, natural_width);
+ }
+}
+
+void CellRendererChannels::get_preferred_width_for_height_vfunc(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
+ switch (m_property_type.get_value()) {
+ case RenderType::Guild:
+ return get_preferred_width_for_height_vfunc_guild(widget, height, minimum_width, natural_width);
+ case RenderType::Category:
+ return get_preferred_width_for_height_vfunc_category(widget, height, minimum_width, natural_width);
+ case RenderType::TextChannel:
+ return get_preferred_width_for_height_vfunc_channel(widget, height, minimum_width, natural_width);
+ case RenderType::Thread:
+ return get_preferred_width_for_height_vfunc_thread(widget, height, minimum_width, natural_width);
+ case RenderType::DMHeader:
+ return get_preferred_width_for_height_vfunc_dmheader(widget, height, minimum_width, natural_width);
+ case RenderType::DM:
+ return get_preferred_width_for_height_vfunc_dm(widget, height, minimum_width, natural_width);
+ }
+}
+
+void CellRendererChannels::get_preferred_height_vfunc(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
+ switch (m_property_type.get_value()) {
+ case RenderType::Guild:
+ return get_preferred_height_vfunc_guild(widget, minimum_height, natural_height);
+ case RenderType::Category:
+ return get_preferred_height_vfunc_category(widget, minimum_height, natural_height);
+ case RenderType::TextChannel:
+ return get_preferred_height_vfunc_channel(widget, minimum_height, natural_height);
+ case RenderType::Thread:
+ return get_preferred_height_vfunc_thread(widget, minimum_height, natural_height);
+ case RenderType::DMHeader:
+ return get_preferred_height_vfunc_dmheader(widget, minimum_height, natural_height);
+ case RenderType::DM:
+ return get_preferred_height_vfunc_dm(widget, minimum_height, natural_height);
+ }
+}
+
+void CellRendererChannels::get_preferred_height_for_width_vfunc(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
+ switch (m_property_type.get_value()) {
+ case RenderType::Guild:
+ return get_preferred_height_for_width_vfunc_guild(widget, width, minimum_height, natural_height);
+ case RenderType::Category:
+ return get_preferred_height_for_width_vfunc_category(widget, width, minimum_height, natural_height);
+ case RenderType::TextChannel:
+ return get_preferred_height_for_width_vfunc_channel(widget, width, minimum_height, natural_height);
+ case RenderType::Thread:
+ return get_preferred_height_for_width_vfunc_thread(widget, width, minimum_height, natural_height);
+ case RenderType::DMHeader:
+ return get_preferred_height_for_width_vfunc_dmheader(widget, width, minimum_height, natural_height);
+ case RenderType::DM:
+ return get_preferred_height_for_width_vfunc_dm(widget, width, minimum_height, natural_height);
+ }
+}
+
+void CellRendererChannels::render_vfunc(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
+ switch (m_property_type.get_value()) {
+ case RenderType::Guild:
+ return render_vfunc_guild(cr, widget, background_area, cell_area, flags);
+ case RenderType::Category:
+ return render_vfunc_category(cr, widget, background_area, cell_area, flags);
+ case RenderType::TextChannel:
+ return render_vfunc_channel(cr, widget, background_area, cell_area, flags);
+ case RenderType::Thread:
+ return render_vfunc_thread(cr, widget, background_area, cell_area, flags);
+ case RenderType::DMHeader:
+ return render_vfunc_dmheader(cr, widget, background_area, cell_area, flags);
+ case RenderType::DM:
+ return render_vfunc_dm(cr, widget, background_area, cell_area, flags);
+ }
+}
+
+// guild functions
+
+void CellRendererChannels::get_preferred_width_vfunc_guild(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
+ int pixbuf_width = 0;
+
+ if (auto pixbuf = m_property_pixbuf_animation.get_value())
+ pixbuf_width = pixbuf->get_width();
+ else if (auto pixbuf = m_property_pixbuf.get_value())
+ pixbuf_width = pixbuf->get_width();
+
+ int text_min, text_nat;
+ m_renderer_text.get_preferred_width(widget, text_min, text_nat);
+
+ int xpad, ypad;
+ get_padding(xpad, ypad);
+ minimum_width = std::max(text_min, pixbuf_width) + xpad * 2;
+ natural_width = std::max(text_nat, pixbuf_width) + xpad * 2;
+}
+
+void CellRendererChannels::get_preferred_width_for_height_vfunc_guild(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
+ get_preferred_width_vfunc_guild(widget, minimum_width, natural_width);
+}
+
+void CellRendererChannels::get_preferred_height_vfunc_guild(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
+ int pixbuf_height = 0;
+ if (auto pixbuf = m_property_pixbuf_animation.get_value())
+ pixbuf_height = pixbuf->get_height();
+ else if (auto pixbuf = m_property_pixbuf.get_value())
+ pixbuf_height = pixbuf->get_height();
+
+ int text_min, text_nat;
+ m_renderer_text.get_preferred_height(widget, text_min, text_nat);
+
+ int xpad, ypad;
+ get_padding(xpad, ypad);
+ minimum_height = std::max(text_min, pixbuf_height) + ypad * 2;
+ natural_height = std::max(text_nat, pixbuf_height) + ypad * 2;
+}
+
+void CellRendererChannels::get_preferred_height_for_width_vfunc_guild(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
+ get_preferred_height_vfunc_guild(widget, minimum_height, natural_height);
+}
+
+void CellRendererChannels::render_vfunc_guild(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
+ Gtk::Requisition text_minimum, text_natural;
+ m_renderer_text.get_preferred_size(widget, text_minimum, text_natural);
+
+ Gtk::Requisition minimum, natural;
+ get_preferred_size(widget, minimum, natural);
+
+ int pixbuf_w, pixbuf_h = 0;
+ if (auto pixbuf = m_property_pixbuf_animation.get_value()) {
+ pixbuf_w = pixbuf->get_width();
+ pixbuf_h = pixbuf->get_height();
+ } else if (auto pixbuf = m_property_pixbuf.get_value()) {
+ pixbuf_w = pixbuf->get_width();
+ pixbuf_h = pixbuf->get_height();
+ }
+
+ const double icon_w = pixbuf_w;
+ const double icon_h = pixbuf_h;
+ const double icon_x = background_area.get_x();
+ const double icon_y = background_area.get_y() + background_area.get_height() / 2.0 - icon_h / 2.0;
+
+ const double text_x = icon_x + icon_w + 5.0;
+ const double text_y = background_area.get_y() + background_area.get_height() / 2.0 - text_natural.height / 2.0;
+ const double text_w = text_natural.width;
+ const double text_h = text_natural.height;
+
+ Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h);
+
+ m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
+
+ const static bool hover_only = Abaddon::Get().GetSettings().GetAnimatedGuildHoverOnly();
+ const bool is_hovered = flags & Gtk::CELL_RENDERER_PRELIT;
+ auto anim = m_property_pixbuf_animation.get_value();
+
+ // kinda gross
+ if (anim) {
+ auto map_iter = m_pixbuf_anim_iters.find(anim);
+ if (map_iter == m_pixbuf_anim_iters.end())
+ m_pixbuf_anim_iters[anim] = anim->get_iter(nullptr);
+ auto pb_iter = m_pixbuf_anim_iters.at(anim);
+
+ const auto cb = [this, &widget, anim, icon_x, icon_y, icon_w, icon_h] {
+ if (m_pixbuf_anim_iters.at(anim)->advance())
+ widget.queue_draw_area(icon_x, icon_y, icon_w, icon_h);
+ };
+
+ if ((hover_only && is_hovered) || !hover_only)
+ Glib::signal_timeout().connect_once(sigc::track_obj(cb, widget), pb_iter->get_delay_time());
+ if (hover_only && !is_hovered)
+ m_pixbuf_anim_iters[anim] = anim->get_iter(nullptr);
+
+ Gdk::Cairo::set_source_pixbuf(cr, pb_iter->get_pixbuf(), icon_x, icon_y);
+ cr->rectangle(icon_x, icon_y, icon_w, icon_h);
+ cr->fill();
+ } else if (auto pixbuf = m_property_pixbuf.get_value()) {
+ Gdk::Cairo::set_source_pixbuf(cr, pixbuf, icon_x, icon_y);
+ cr->rectangle(icon_x, icon_y, icon_w, icon_h);
+ cr->fill();
+ }
+}
+
+// category
+
+void CellRendererChannels::get_preferred_width_vfunc_category(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
+ m_renderer_text.get_preferred_width(widget, minimum_width, natural_width);
+}
+
+void CellRendererChannels::get_preferred_width_for_height_vfunc_category(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
+ m_renderer_text.get_preferred_width_for_height(widget, height, minimum_width, natural_width);
+}
+
+void CellRendererChannels::get_preferred_height_vfunc_category(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
+ m_renderer_text.get_preferred_height(widget, minimum_height, natural_height);
+}
+
+void CellRendererChannels::get_preferred_height_for_width_vfunc_category(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
+ m_renderer_text.get_preferred_height_for_width(widget, width, minimum_height, natural_height);
+}
+
+void CellRendererChannels::render_vfunc_category(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
+ // todo: figure out how Gtk::Arrow is rendered because i like it better :^)
+ constexpr static int len = 5;
+ int x1, y1, x2, y2, x3, y3;
+ if (property_expanded()) {
+ x1 = background_area.get_x() + 7;
+ y1 = background_area.get_y() + background_area.get_height() / 2 - len;
+ x2 = background_area.get_x() + 7 + len;
+ y2 = background_area.get_y() + background_area.get_height() / 2 + len;
+ x3 = background_area.get_x() + 7 + len * 2;
+ y3 = background_area.get_y() + background_area.get_height() / 2 - len;
+ } else {
+ x1 = background_area.get_x() + 7;
+ y1 = background_area.get_y() + background_area.get_height() / 2 - len;
+ x2 = background_area.get_x() + 7 + len * 2;
+ y2 = background_area.get_y() + background_area.get_height() / 2;
+ x3 = background_area.get_x() + 7;
+ y3 = background_area.get_y() + background_area.get_height() / 2 + len;
+ }
+ cr->move_to(x1, y1);
+ cr->line_to(x2, y2);
+ cr->line_to(x3, y3);
+ static const auto expander_color = Gdk::RGBA(Abaddon::Get().GetSettings().GetChannelsExpanderColor());
+ cr->set_source_rgb(expander_color.get_red(), expander_color.get_green(), expander_color.get_blue());
+ cr->stroke();
+
+ Gtk::Requisition text_minimum, text_natural;
+ m_renderer_text.get_preferred_size(widget, text_minimum, text_natural);
+
+ const int text_x = background_area.get_x() + 22;
+ const int text_y = background_area.get_y() + background_area.get_height() / 2 - text_natural.height / 2;
+ const int text_w = text_natural.width;
+ const int text_h = text_natural.height;
+
+ Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h);
+
+ m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
+}
+
+// text channel
+
+void CellRendererChannels::get_preferred_width_vfunc_channel(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
+ m_renderer_text.get_preferred_width(widget, minimum_width, natural_width);
+}
+
+void CellRendererChannels::get_preferred_width_for_height_vfunc_channel(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
+ m_renderer_text.get_preferred_width_for_height(widget, height, minimum_width, natural_width);
+}
+
+void CellRendererChannels::get_preferred_height_vfunc_channel(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
+ m_renderer_text.get_preferred_height(widget, minimum_height, natural_height);
+}
+
+void CellRendererChannels::get_preferred_height_for_width_vfunc_channel(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
+ m_renderer_text.get_preferred_height_for_width(widget, width, minimum_height, natural_height);
+}
+
+void CellRendererChannels::render_vfunc_channel(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
+ Gtk::Requisition minimum_size, natural_size;
+ m_renderer_text.get_preferred_size(widget, minimum_size, natural_size);
+
+ const int text_x = background_area.get_x() + 21;
+ const int text_y = background_area.get_y() + background_area.get_height() / 2 - natural_size.height / 2;
+ const int text_w = natural_size.width;
+ const int text_h = natural_size.height;
+
+ Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h);
+
+ const static auto nsfw_color = Gdk::RGBA(Abaddon::Get().GetSettings().GetNSFWChannelColor());
+ if (m_property_nsfw.get_value())
+ m_renderer_text.property_foreground_rgba() = nsfw_color;
+ m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
+ // setting property_foreground_rgba() sets this to true which makes non-nsfw cells use the property too which is bad
+ // so unset it
+ m_renderer_text.property_foreground_set() = false;
+}
+
+// thread
+
+void CellRendererChannels::get_preferred_width_vfunc_thread(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
+ m_renderer_text.get_preferred_width(widget, minimum_width, natural_width);
+}
+
+void CellRendererChannels::get_preferred_width_for_height_vfunc_thread(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
+ get_preferred_width_vfunc_thread(widget, minimum_width, natural_width);
+}
+
+void CellRendererChannels::get_preferred_height_vfunc_thread(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
+ m_renderer_text.get_preferred_height(widget, minimum_height, natural_height);
+}
+
+void CellRendererChannels::get_preferred_height_for_width_vfunc_thread(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
+ get_preferred_height_vfunc_thread(widget, minimum_height, natural_height);
+}
+
+void CellRendererChannels::render_vfunc_thread(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
+ Gtk::Requisition minimum_size, natural_size;
+ m_renderer_text.get_preferred_size(widget, minimum_size, natural_size);
+
+ const int text_x = background_area.get_x() + 26;
+ const int text_y = background_area.get_y() + background_area.get_height() / 2 - natural_size.height / 2;
+ const int text_w = natural_size.width;
+ const int text_h = natural_size.height;
+
+ Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h);
+ m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
+}
+
+// dm header
+
+void CellRendererChannels::get_preferred_width_vfunc_dmheader(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
+ m_renderer_text.get_preferred_width(widget, minimum_width, natural_width);
+}
+
+void CellRendererChannels::get_preferred_width_for_height_vfunc_dmheader(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
+ m_renderer_text.get_preferred_width_for_height(widget, height, minimum_width, natural_width);
+}
+
+void CellRendererChannels::get_preferred_height_vfunc_dmheader(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
+ m_renderer_text.get_preferred_height(widget, minimum_height, natural_height);
+}
+
+void CellRendererChannels::get_preferred_height_for_width_vfunc_dmheader(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
+ m_renderer_text.get_preferred_height_for_width(widget, width, minimum_height, natural_height);
+}
+
+void CellRendererChannels::render_vfunc_dmheader(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
+ // gdk::rectangle more like gdk::stupid
+ Gdk::Rectangle text_cell_area(
+ cell_area.get_x() + 9, cell_area.get_y(), // maybe theres a better way to align this ?
+ cell_area.get_width(), cell_area.get_height());
+ m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
+}
+
+// dm (basically the same thing as guild)
+
+void CellRendererChannels::get_preferred_width_vfunc_dm(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
+ int pixbuf_width = 0;
+ if (auto pixbuf = m_property_pixbuf.get_value())
+ pixbuf_width = pixbuf->get_width();
+
+ int text_min, text_nat;
+ m_renderer_text.get_preferred_width(widget, text_min, text_nat);
+
+ int xpad, ypad;
+ get_padding(xpad, ypad);
+ minimum_width = std::max(text_min, pixbuf_width) + xpad * 2;
+ natural_width = std::max(text_nat, pixbuf_width) + xpad * 2;
+}
+
+void CellRendererChannels::get_preferred_width_for_height_vfunc_dm(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
+ get_preferred_width_vfunc_guild(widget, minimum_width, natural_width);
+}
+
+void CellRendererChannels::get_preferred_height_vfunc_dm(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
+ int pixbuf_height = 0;
+ if (auto pixbuf = m_property_pixbuf.get_value())
+ pixbuf_height = pixbuf->get_height();
+
+ int text_min, text_nat;
+ m_renderer_text.get_preferred_height(widget, text_min, text_nat);
+
+ int xpad, ypad;
+ get_padding(xpad, ypad);
+ minimum_height = std::max(text_min, pixbuf_height) + ypad * 2;
+ natural_height = std::max(text_nat, pixbuf_height) + ypad * 2;
+}
+
+void CellRendererChannels::get_preferred_height_for_width_vfunc_dm(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
+ get_preferred_height_vfunc_guild(widget, minimum_height, natural_height);
+}
+
+void CellRendererChannels::render_vfunc_dm(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
+ Gtk::Requisition text_minimum, text_natural;
+ m_renderer_text.get_preferred_size(widget, text_minimum, text_natural);
+
+ Gtk::Requisition minimum, natural;
+ get_preferred_size(widget, minimum, natural);
+
+ auto pixbuf = m_property_pixbuf.get_value();
+
+ const double icon_w = pixbuf->get_width();
+ const double icon_h = pixbuf->get_height();
+ const double icon_x = background_area.get_x() + 2;
+ const double icon_y = background_area.get_y() + background_area.get_height() / 2.0 - icon_h / 2.0;
+
+ const double text_x = icon_x + icon_w + 5.0;
+ const double text_y = background_area.get_y() + background_area.get_height() / 2.0 - text_natural.height / 2.0;
+ const double text_w = text_natural.width;
+ const double text_h = text_natural.height;
+
+ Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h);
+
+ m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
+
+ Gdk::Cairo::set_source_pixbuf(cr, m_property_pixbuf.get_value(), icon_x, icon_y);
+ cr->rectangle(icon_x, icon_y, icon_w, icon_h);
+ cr->fill();
+}