From 3e0cd83dd6026dc701c548dc40b41f5accbc29db Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Mon, 16 Oct 2023 21:31:28 -0400 Subject: start channel list refactor for server list --- .../channellist/cellrendererchannels.cpp | 970 ++++++++++++++ .../channellist/cellrendererchannels.hpp | 174 +++ src/components/channellist/channellist.cpp | 1317 +++++++++++++++++++ src/components/channellist/channellist.hpp | 231 ++++ src/components/channellist/classic/guildlist.cpp | 23 + src/components/channellist/classic/guildlist.hpp | 12 + .../channellist/classic/guildlistfolderitem.cpp | 18 + .../channellist/classic/guildlistfolderitem.hpp | 18 + .../channellist/classic/guildlistguilditem.cpp | 29 + .../channellist/classic/guildlistguilditem.hpp | 17 + src/components/channels.cpp | 1352 -------------------- src/components/channels.hpp | 229 ---- src/components/channelscellrenderer.cpp | 970 -------------- src/components/channelscellrenderer.hpp | 174 --- 14 files changed, 2809 insertions(+), 2725 deletions(-) create mode 100644 src/components/channellist/cellrendererchannels.cpp create mode 100644 src/components/channellist/cellrendererchannels.hpp create mode 100644 src/components/channellist/channellist.cpp create mode 100644 src/components/channellist/channellist.hpp create mode 100644 src/components/channellist/classic/guildlist.cpp create mode 100644 src/components/channellist/classic/guildlist.hpp create mode 100644 src/components/channellist/classic/guildlistfolderitem.cpp create mode 100644 src/components/channellist/classic/guildlistfolderitem.hpp create mode 100644 src/components/channellist/classic/guildlistguilditem.cpp create mode 100644 src/components/channellist/classic/guildlistguilditem.hpp delete mode 100644 src/components/channels.cpp delete mode 100644 src/components/channels.hpp delete mode 100644 src/components/channelscellrenderer.cpp delete mode 100644 src/components/channelscellrenderer.hpp (limited to 'src/components') diff --git a/src/components/channellist/cellrendererchannels.cpp b/src/components/channellist/cellrendererchannels.cpp new file mode 100644 index 0000000..e0079cd --- /dev/null +++ b/src/components/channellist/cellrendererchannels.cpp @@ -0,0 +1,970 @@ +#include "cellrendererchannels.hpp" + +constexpr static int MentionsRightPad = 7; +#ifndef M_PI +constexpr static double M_PI = 3.14159265358979; +#endif +constexpr static double M_PI_H = M_PI / 2.0; +constexpr static double M_PI_3_2 = M_PI * 3.0 / 2.0; + +CellRendererChannels::CellRendererChannels() + : Glib::ObjectBase(typeid(CellRendererChannels)) + , Gtk::CellRenderer() + , m_property_type(*this, "render-type") + , m_property_id(*this, "id") + , 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") + , m_property_color(*this, "color") + , m_property_voice_state(*this, "voice-state") { + 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; + }); +} + +Glib::PropertyProxy CellRendererChannels::property_type() { + return m_property_type.get_proxy(); +} + +Glib::PropertyProxy CellRendererChannels::property_id() { + return m_property_id.get_proxy(); +} + +Glib::PropertyProxy CellRendererChannels::property_name() { + return m_property_name.get_proxy(); +} + +Glib::PropertyProxy> CellRendererChannels::property_icon() { + return m_property_pixbuf.get_proxy(); +} + +Glib::PropertyProxy> CellRendererChannels::property_icon_animation() { + return m_property_pixbuf_animation.get_proxy(); +} + +Glib::PropertyProxy CellRendererChannels::property_expanded() { + return m_property_expanded.get_proxy(); +} + +Glib::PropertyProxy CellRendererChannels::property_nsfw() { + return m_property_nsfw.get_proxy(); +} + +Glib::PropertyProxy> CellRendererChannels::property_color() { + return m_property_color.get_proxy(); +} + +Glib::PropertyProxy CellRendererChannels::property_voice_state() { + return m_property_voice_state.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::Folder: + return get_preferred_width_vfunc_folder(widget, minimum_width, natural_width); + 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); +#ifdef WITH_VOICE + case RenderType::VoiceChannel: + return get_preferred_width_vfunc_voice_channel(widget, minimum_width, natural_width); + case RenderType::VoiceParticipant: + return get_preferred_width_vfunc_voice_participant(widget, minimum_width, natural_width); +#endif + 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::Folder: + return get_preferred_width_for_height_vfunc_folder(widget, height, minimum_width, natural_width); + 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); +#ifdef WITH_VOICE + case RenderType::VoiceChannel: + return get_preferred_width_for_height_vfunc_voice_channel(widget, height, minimum_width, natural_width); + case RenderType::VoiceParticipant: + return get_preferred_width_for_height_vfunc_voice_participant(widget, height, minimum_width, natural_width); +#endif + 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::Folder: + return get_preferred_height_vfunc_folder(widget, minimum_height, natural_height); + 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); +#ifdef WITH_VOICE + case RenderType::VoiceChannel: + return get_preferred_height_vfunc_voice_channel(widget, minimum_height, natural_height); + case RenderType::VoiceParticipant: + return get_preferred_height_vfunc_voice_participant(widget, minimum_height, natural_height); +#endif + 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::Folder: + return get_preferred_height_for_width_vfunc_folder(widget, width, minimum_height, natural_height); + 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); +#ifdef WITH_VOICE + case RenderType::VoiceChannel: + return get_preferred_height_for_width_vfunc_voice_channel(widget, width, minimum_height, natural_height); + case RenderType::VoiceParticipant: + return get_preferred_height_for_width_vfunc_voice_participant(widget, width, minimum_height, natural_height); +#endif + 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 &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::Folder: + return render_vfunc_folder(cr, widget, background_area, cell_area, flags); + 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); +#ifdef WITH_VOICE + case RenderType::VoiceChannel: + return render_vfunc_voice_channel(cr, widget, background_area, cell_area, flags); + case RenderType::VoiceParticipant: + return render_vfunc_voice_participant(cr, widget, background_area, cell_area, flags); +#endif + 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); + } +} + +// folder functions + +void CellRendererChannels::get_preferred_width_vfunc_folder(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_folder(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_folder(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_folder(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_folder(const Cairo::RefPtr &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) { + 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); + const auto expander_color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelsExpanderColor); + 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); + + static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelColor); + if (m_property_color.get_value().has_value()) { + m_renderer_text.property_foreground_rgba() = *m_property_color.get_value(); + } else { + m_renderer_text.property_foreground_rgba() = color; + } + m_renderer_text.render(cr, widget, background_area, text_cell_area, flags); + m_renderer_text.property_foreground_set() = false; +} + +// 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 &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() + 3; + 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(static_cast(text_x), + static_cast(text_y), + static_cast(text_w), + static_cast(text_h)); + + static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelColor); + m_renderer_text.property_foreground_rgba() = color; + m_renderer_text.render(cr, widget, background_area, text_cell_area, flags); + + const bool hover_only = Abaddon::Get().GetSettings().AnimatedGuildHoverOnly; + 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( + static_cast(icon_x), + static_cast(icon_y), + static_cast(icon_w), + static_cast(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(); + } + + // unread + if (!Abaddon::Get().GetSettings().Unreads) return; + + const auto id = m_property_id.get_value(); + + auto &discord = Abaddon::Get().GetDiscordClient(); + int total_mentions; + const auto has_unread = discord.GetUnreadStateForGuild(id, total_mentions); + + if (has_unread && !discord.IsGuildMuted(id)) { + static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().UnreadIndicatorColor); + cr->set_source_rgb(color.get_red(), color.get_green(), color.get_blue()); + const auto x = background_area.get_x(); + const auto y = background_area.get_y(); + const auto w = background_area.get_width(); + const auto h = background_area.get_height(); + cr->rectangle(x, y + h / 2.0 - 24.0 / 2.0, 3.0, 24.0); + cr->fill(); + } + + if (total_mentions < 1) return; + auto *paned = dynamic_cast(widget.get_ancestor(Gtk::Paned::get_type())); + if (paned != nullptr) { + const auto edge = std::min(paned->get_position(), background_area.get_width()); + + unread_render_mentions(cr, widget, total_mentions, edge, background_area); + } +} + +// 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 AddUnreadIndicator(const Cairo::RefPtr &cr, const Gdk::Rectangle &background_area) { + static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().UnreadIndicatorColor); + cr->set_source_rgb(color.get_red(), color.get_green(), color.get_blue()); + const auto x = background_area.get_x(); + const auto y = background_area.get_y(); + const auto w = background_area.get_width(); + const auto h = background_area.get_height(); + cr->rectangle(x, y, 3, h); + cr->fill(); +} + +void CellRendererChannels::render_vfunc_category(const Cairo::RefPtr &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); + const auto expander_color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelsExpanderColor); + 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); + + static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelColor); + auto &discord = Abaddon::Get().GetDiscordClient(); + const auto id = m_property_id.get_value(); + if (discord.IsChannelMuted(m_property_id.get_value())) { + auto muted = color; + muted.set_red(muted.get_red() * 0.5); + muted.set_green(muted.get_green() * 0.5); + muted.set_blue(muted.get_blue() * 0.5); + m_renderer_text.property_foreground_rgba() = muted; + } else { + if (discord.GetUnreadChannelsCountForCategory(id) > 0) { + AddUnreadIndicator(cr, background_area); + } + m_renderer_text.property_foreground_rgba() = color; + } + m_renderer_text.render(cr, widget, background_area, text_cell_area, flags); + m_renderer_text.property_foreground_set() = false; +} + +// 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 &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); + + auto &discord = Abaddon::Get().GetDiscordClient(); + const auto id = m_property_id.get_value(); + const bool is_muted = discord.IsChannelMuted(id); + + static const auto sfw_unmuted = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelColor); + + m_renderer_text.property_sensitive() = false; + static const auto nsfw_color = Gdk::RGBA(Abaddon::Get().GetSettings().NSFWChannelColor); + if (m_property_nsfw.get_value()) + m_renderer_text.property_foreground_rgba() = nsfw_color; + else + m_renderer_text.property_foreground_rgba() = sfw_unmuted; + if (is_muted) { + auto col = m_renderer_text.property_foreground_rgba().get_value(); + col.set_red(col.get_red() * 0.5); + col.set_green(col.get_green() * 0.5); + col.set_blue(col.get_blue() * 0.5); + m_renderer_text.property_foreground_rgba() = col; + } + m_renderer_text.render(cr, widget, background_area, text_cell_area, flags); + // unset foreground to default so properties dont bleed + m_renderer_text.property_foreground_set() = false; + + // unread + if (!Abaddon::Get().GetSettings().Unreads) return; + + const auto unread_state = discord.GetUnreadStateForChannel(id); + if (unread_state < 0) return; + + if (!is_muted) { + AddUnreadIndicator(cr, background_area); + } + + if (unread_state < 1) return; + auto *paned = dynamic_cast(widget.get_ancestor(Gtk::Paned::get_type())); + if (paned != nullptr) { + const auto edge = std::min(paned->get_position(), cell_area.get_width()); + + unread_render_mentions(cr, widget, unread_state, edge, cell_area); + } +} + +// 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 &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); + + auto &discord = Abaddon::Get().GetDiscordClient(); + const auto id = m_property_id.get_value(); + const bool is_muted = discord.IsChannelMuted(id); + + static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelColor); + if (Abaddon::Get().GetDiscordClient().IsChannelMuted(m_property_id.get_value())) { + auto muted = color; + muted.set_red(muted.get_red() * 0.5); + muted.set_green(muted.get_green() * 0.5); + muted.set_blue(muted.get_blue() * 0.5); + m_renderer_text.property_foreground_rgba() = muted; + } else { + m_renderer_text.property_foreground_rgba() = color; + } + m_renderer_text.render(cr, widget, background_area, text_cell_area, flags); + m_renderer_text.property_foreground_set() = false; + + // unread + if (!Abaddon::Get().GetSettings().Unreads) return; + + const auto unread_state = discord.GetUnreadStateForChannel(id); + if (unread_state < 0) return; + + if (!is_muted) { + static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().UnreadIndicatorColor); + cr->set_source_rgb(color.get_red(), color.get_green(), color.get_blue()); + const auto x = background_area.get_x(); + const auto y = background_area.get_y(); + const auto w = background_area.get_width(); + const auto h = background_area.get_height(); + cr->rectangle(x, y, 3, h); + cr->fill(); + } + + if (unread_state < 1) return; + auto *paned = dynamic_cast(widget.get_ancestor(Gtk::Paned::get_type())); + if (paned != nullptr) { + const auto edge = std::min(paned->get_position(), cell_area.get_width()); + + unread_render_mentions(cr, widget, unread_state, edge, cell_area); + } +} + +#ifdef WITH_VOICE + +// voice channel + +void CellRendererChannels::get_preferred_width_vfunc_voice_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_voice_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_voice_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_voice_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_voice_channel(const Cairo::RefPtr &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) { + // channel name text + 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() + 35; + 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); + + // speaker character + Pango::FontDescription font; + font.set_family("sans 14"); + + auto layout = widget.create_pango_layout("\U0001F50A"); + layout->set_font_description(font); + layout->set_alignment(Pango::ALIGN_LEFT); + cr->set_source_rgba(1.0, 1.0, 1.0, 1.0); + int width, height; + layout->get_pixel_size(width, height); + cr->move_to( + background_area.get_x() + 1, + cell_area.get_y() + cell_area.get_height() / 2.0 - height / 2.0); + layout->show_in_cairo_context(cr); + + // expander + constexpr static int len = 5; + constexpr static int offset = 24; + int x1, y1, x2, y2, x3, y3; + if (property_expanded()) { + x1 = background_area.get_x() + offset; + y1 = background_area.get_y() + background_area.get_height() / 2 - len; + x2 = background_area.get_x() + offset + len; + y2 = background_area.get_y() + background_area.get_height() / 2 + len; + x3 = background_area.get_x() + offset + len * 2; + y3 = background_area.get_y() + background_area.get_height() / 2 - len; + } else { + x1 = background_area.get_x() + offset; + y1 = background_area.get_y() + background_area.get_height() / 2 - len; + x2 = background_area.get_x() + offset + len * 2; + y2 = background_area.get_y() + background_area.get_height() / 2; + x3 = background_area.get_x() + offset; + 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); + const auto expander_color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelsExpanderColor); + cr->set_source_rgb(expander_color.get_red(), expander_color.get_green(), expander_color.get_blue()); + cr->stroke(); +} + +// voice participant + +void CellRendererChannels::get_preferred_width_vfunc_voice_participant(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_voice_participant(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_voice_participant(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_voice_participant(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_voice_participant(const Cairo::RefPtr &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 = 0; + int pixbuf_h = 0; + + 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() + 28; + 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.property_scale() = Pango::SCALE_SMALL; + m_renderer_text.render(cr, widget, background_area, text_cell_area, flags); + m_renderer_text.property_scale_set() = false; + + 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(); + } + + auto *paned = dynamic_cast(widget.get_ancestor(Gtk::Paned::get_type())); + if (paned != nullptr) { + const auto edge = std::min(paned->get_position(), background_area.get_width()); + + const static std::array, 3> icon_order = { { + { VoiceStateFlags::SelfMute | VoiceStateFlags::Mute, "microphone-disabled-symbolic" }, + { VoiceStateFlags::SelfDeaf | VoiceStateFlags::Deaf, "audio-volume-muted-symbolic" }, + { VoiceStateFlags::SelfVideo, "camera-web-symbolic" }, + } }; + + constexpr static int IconSize = 18; + constexpr static int IconPad = 2; + + const VoiceStateFlags voice_flags = m_property_voice_state.get_value(); + + int offset = 0; + for (auto iter = icon_order.rbegin(); iter != icon_order.rend(); iter++) { + const auto &[flag, icon] = *iter; + if ((voice_flags & flag) == VoiceStateFlags::Clear) continue; + + const double icon_w = 18; + const double icon_h = 18; + const double icon_x = background_area.get_x() + edge - icon_w + offset; + const double icon_y = background_area.get_y() + background_area.get_height() / 2 - icon_h / 2; + Gdk::Rectangle icon_cell_area(icon_x, icon_y, icon_w, icon_h); + + offset -= (IconSize + IconPad); + + const bool is_server_mute = (voice_flags & VoiceStateFlags::Mute) == VoiceStateFlags::Mute; + const bool is_server_deaf = (voice_flags & VoiceStateFlags::Deaf) == VoiceStateFlags::Deaf; + auto context = widget.get_style_context(); + if (is_server_mute || is_server_deaf) { + context->context_save(); + context->add_class("voice-state-server"); + } + + m_renderer_pixbuf.property_icon_name() = icon; + m_renderer_pixbuf.render(cr, widget, background_area, icon_cell_area, flags); + + if (is_server_mute || is_server_deaf) { + context->context_restore(); + } + } + } +} + +#endif + +// 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 &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); + + if (!Abaddon::Get().GetSettings().Unreads) return; + + auto *paned = dynamic_cast(widget.get_ancestor(Gtk::Paned::get_type())); + if (paned != nullptr) { + const auto edge = std::min(paned->get_position(), background_area.get_width()); + if (const auto unread = Abaddon::Get().GetDiscordClient().GetUnreadDMsCount(); unread > 0) + unread_render_mentions(cr, widget, unread, edge, background_area); + } +} + +// 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 &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() + 3; + 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 + 6.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(static_cast(text_x), + static_cast(text_y), + static_cast(text_w), + static_cast(text_h)); + + auto &discord = Abaddon::Get().GetDiscordClient(); + const auto id = m_property_id.get_value(); + const bool is_muted = discord.IsChannelMuted(id); + + static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelColor); + if (Abaddon::Get().GetDiscordClient().IsChannelMuted(m_property_id.get_value())) { + auto muted = color; + muted.set_red(muted.get_red() * 0.5); + muted.set_green(muted.get_green() * 0.5); + muted.set_blue(muted.get_blue() * 0.5); + m_renderer_text.property_foreground_rgba() = muted; + } else { + m_renderer_text.property_foreground_rgba() = color; + } + m_renderer_text.render(cr, widget, background_area, text_cell_area, flags); + m_renderer_text.property_foreground_set() = false; + + 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(); + + // unread + if (!Abaddon::Get().GetSettings().Unreads) return; + + const auto unread_state = discord.GetUnreadStateForChannel(id); + if (unread_state < 0) return; + + if (!is_muted) { + static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().UnreadIndicatorColor); + cr->set_source_rgb(color.get_red(), color.get_green(), color.get_blue()); + const auto x = background_area.get_x(); + const auto y = background_area.get_y(); + const auto w = background_area.get_width(); + const auto h = background_area.get_height(); + cr->rectangle(x, y, 3, h); + cr->fill(); + } +} + +void CellRendererChannels::cairo_path_rounded_rect(const Cairo::RefPtr &cr, double x, double y, double w, double h, double r) { + const double degrees = M_PI / 180.0; + + cr->begin_new_sub_path(); + cr->arc(x + w - r, y + r, r, -M_PI_H, 0); + cr->arc(x + w - r, y + h - r, r, 0, M_PI_H); + cr->arc(x + r, y + h - r, r, M_PI_H, M_PI); + cr->arc(x + r, y + r, r, M_PI, M_PI_3_2); + cr->close_path(); +} + +void CellRendererChannels::unread_render_mentions(const Cairo::RefPtr &cr, Gtk::Widget &widget, int mentions, int edge, const Gdk::Rectangle &cell_area) { + Pango::FontDescription font; + font.set_family("sans 14"); + // font.set_weight(Pango::WEIGHT_BOLD); + + auto layout = widget.create_pango_layout(std::to_string(mentions)); + layout->set_font_description(font); + layout->set_alignment(Pango::ALIGN_RIGHT); + + int width, height; + layout->get_pixel_size(width, height); + { + static const auto bg = Gdk::RGBA(Abaddon::Get().GetSettings().MentionBadgeColor); + static const auto text = Gdk::RGBA(Abaddon::Get().GetSettings().MentionBadgeTextColor); + + const auto x = cell_area.get_x() + edge - width - MentionsRightPad; + const auto y = cell_area.get_y() + cell_area.get_height() / 2.0 - height / 2.0 - 1; + cairo_path_rounded_rect(cr, x - 4, y + 2, width + 8, height, 5); + cr->set_source_rgb(bg.get_red(), bg.get_green(), bg.get_blue()); + cr->fill(); + cr->set_source_rgb(text.get_red(), text.get_green(), text.get_blue()); + cr->move_to(x, y); + layout->show_in_cairo_context(cr); + } +} diff --git a/src/components/channellist/cellrendererchannels.hpp b/src/components/channellist/cellrendererchannels.hpp new file mode 100644 index 0000000..934ce5b --- /dev/null +++ b/src/components/channellist/cellrendererchannels.hpp @@ -0,0 +1,174 @@ +#pragma once +#include +#include +#include +#include "discord/snowflake.hpp" +#include "discord/voicestateflags.hpp" +#include "misc/bitwise.hpp" + +enum class RenderType : uint8_t { + Folder, + Guild, + Category, + TextChannel, + Thread, + +// TODO: maybe enable anyways but without ability to join if no voice support +#ifdef WITH_VOICE + VoiceChannel, + VoiceParticipant, +#endif + + DMHeader, + DM, +}; + +class CellRendererChannels : public Gtk::CellRenderer { +public: + CellRendererChannels(); + ~CellRendererChannels() override = default; + + Glib::PropertyProxy property_type(); + Glib::PropertyProxy property_id(); + Glib::PropertyProxy property_name(); + Glib::PropertyProxy> property_icon(); + Glib::PropertyProxy> property_icon_animation(); + Glib::PropertyProxy property_expanded(); + Glib::PropertyProxy property_nsfw(); + Glib::PropertyProxy> property_color(); + Glib::PropertyProxy property_voice_state(); + +protected: + void get_preferred_width_vfunc(Gtk::Widget &widget, int &minimum_width, int &natural_width) const override; + void get_preferred_width_for_height_vfunc(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const override; + void get_preferred_height_vfunc(Gtk::Widget &widget, int &minimum_height, int &natural_height) const override; + void get_preferred_height_for_width_vfunc(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const override; + void render_vfunc(const Cairo::RefPtr &cr, + Gtk::Widget &widget, + const Gdk::Rectangle &background_area, + const Gdk::Rectangle &cell_area, + Gtk::CellRendererState flags) override; + + // guild functions + void get_preferred_width_vfunc_folder(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; + void get_preferred_width_for_height_vfunc_folder(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; + void get_preferred_height_vfunc_folder(Gtk::Widget &widget, int &minimum_height, int &natural_height) const; + void get_preferred_height_for_width_vfunc_folder(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const; + void render_vfunc_folder(const Cairo::RefPtr &cr, + Gtk::Widget &widget, + const Gdk::Rectangle &background_area, + const Gdk::Rectangle &cell_area, + Gtk::CellRendererState flags); + + // guild functions + void get_preferred_width_vfunc_guild(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; + void get_preferred_width_for_height_vfunc_guild(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; + void get_preferred_height_vfunc_guild(Gtk::Widget &widget, int &minimum_height, int &natural_height) const; + void get_preferred_height_for_width_vfunc_guild(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const; + void render_vfunc_guild(const Cairo::RefPtr &cr, + Gtk::Widget &widget, + const Gdk::Rectangle &background_area, + const Gdk::Rectangle &cell_area, + Gtk::CellRendererState flags); + + // category + void get_preferred_width_vfunc_category(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; + void get_preferred_width_for_height_vfunc_category(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; + void get_preferred_height_vfunc_category(Gtk::Widget &widget, int &minimum_height, int &natural_height) const; + void get_preferred_height_for_width_vfunc_category(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const; + void render_vfunc_category(const Cairo::RefPtr &cr, + Gtk::Widget &widget, + const Gdk::Rectangle &background_area, + const Gdk::Rectangle &cell_area, + Gtk::CellRendererState flags); + + // text channel + void get_preferred_width_vfunc_channel(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; + void get_preferred_width_for_height_vfunc_channel(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; + void get_preferred_height_vfunc_channel(Gtk::Widget &widget, int &minimum_height, int &natural_height) const; + void get_preferred_height_for_width_vfunc_channel(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const; + void render_vfunc_channel(const Cairo::RefPtr &cr, + Gtk::Widget &widget, + const Gdk::Rectangle &background_area, + const Gdk::Rectangle &cell_area, + Gtk::CellRendererState flags); + + // thread + void get_preferred_width_vfunc_thread(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; + void get_preferred_width_for_height_vfunc_thread(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; + void get_preferred_height_vfunc_thread(Gtk::Widget &widget, int &minimum_height, int &natural_height) const; + void get_preferred_height_for_width_vfunc_thread(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const; + void render_vfunc_thread(const Cairo::RefPtr &cr, + Gtk::Widget &widget, + const Gdk::Rectangle &background_area, + const Gdk::Rectangle &cell_area, + Gtk::CellRendererState flags); + +#ifdef WITH_VOICE + // voice channel + void get_preferred_width_vfunc_voice_channel(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; + void get_preferred_width_for_height_vfunc_voice_channel(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; + void get_preferred_height_vfunc_voice_channel(Gtk::Widget &widget, int &minimum_height, int &natural_height) const; + void get_preferred_height_for_width_vfunc_voice_channel(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const; + void render_vfunc_voice_channel(const Cairo::RefPtr &cr, + Gtk::Widget &widget, + const Gdk::Rectangle &background_area, + const Gdk::Rectangle &cell_area, + Gtk::CellRendererState flags); + + // voice participant + void get_preferred_width_vfunc_voice_participant(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; + void get_preferred_width_for_height_vfunc_voice_participant(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; + void get_preferred_height_vfunc_voice_participant(Gtk::Widget &widget, int &minimum_height, int &natural_height) const; + void get_preferred_height_for_width_vfunc_voice_participant(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const; + void render_vfunc_voice_participant(const Cairo::RefPtr &cr, + Gtk::Widget &widget, + const Gdk::Rectangle &background_area, + const Gdk::Rectangle &cell_area, + Gtk::CellRendererState flags); +#endif + + // dm header + void get_preferred_width_vfunc_dmheader(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; + void get_preferred_width_for_height_vfunc_dmheader(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; + void get_preferred_height_vfunc_dmheader(Gtk::Widget &widget, int &minimum_height, int &natural_height) const; + void get_preferred_height_for_width_vfunc_dmheader(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const; + void render_vfunc_dmheader(const Cairo::RefPtr &cr, + Gtk::Widget &widget, + const Gdk::Rectangle &background_area, + const Gdk::Rectangle &cell_area, + Gtk::CellRendererState flags); + + // dm + void get_preferred_width_vfunc_dm(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; + void get_preferred_width_for_height_vfunc_dm(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; + void get_preferred_height_vfunc_dm(Gtk::Widget &widget, int &minimum_height, int &natural_height) const; + void get_preferred_height_for_width_vfunc_dm(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const; + void render_vfunc_dm(const Cairo::RefPtr &cr, + Gtk::Widget &widget, + const Gdk::Rectangle &background_area, + const Gdk::Rectangle &cell_area, + Gtk::CellRendererState flags); + + static void cairo_path_rounded_rect(const Cairo::RefPtr &cr, double x, double y, double w, double h, double r); + static void unread_render_mentions(const Cairo::RefPtr &cr, Gtk::Widget &widget, int mentions, int edge, const Gdk::Rectangle &cell_area); + +private: + Gtk::CellRendererText m_renderer_text; + Gtk::CellRendererPixbuf m_renderer_pixbuf; + + Glib::Property m_property_type; // all + Glib::Property m_property_name; // all + Glib::Property m_property_id; + Glib::Property> m_property_pixbuf; // guild, dm + Glib::Property> m_property_pixbuf_animation; // guild + Glib::Property m_property_expanded; // category + Glib::Property m_property_nsfw; // channel + Glib::Property> m_property_color; // folder + Glib::Property m_property_voice_state; + + // same pitfalls as in https://github.com/uowuo/abaddon/blob/60404783bd4ce9be26233fe66fc3a74475d9eaa3/components/cellrendererpixbufanimation.hpp#L32-L39 + // this will manifest though since guild icons can change + // an animation or two wont be the end of the world though + std::map, Glib::RefPtr> m_pixbuf_anim_iters; +}; diff --git a/src/components/channellist/channellist.cpp b/src/components/channellist/channellist.cpp new file mode 100644 index 0000000..51e8f33 --- /dev/null +++ b/src/components/channellist/channellist.cpp @@ -0,0 +1,1317 @@ +#include "channellist.hpp" +#include "imgmanager.hpp" +#include "components/statusindicator.hpp" +#include +#include +#include + +ChannelList::ChannelList() + : Glib::ObjectBase(typeid(ChannelList)) + , m_model(Gtk::TreeStore::create(m_columns)) + , m_filter_model(Gtk::TreeModelFilter::create(m_model)) + , m_menu_guild_copy_id("_Copy ID", true) + , m_menu_guild_settings("View _Settings", true) + , m_menu_guild_leave("_Leave", true) + , m_menu_guild_mark_as_read("Mark as _Read", true) + , m_menu_category_copy_id("_Copy ID", true) + , m_menu_channel_copy_id("_Copy ID", true) + , m_menu_channel_mark_as_read("Mark as _Read", true) +#ifdef WITH_LIBHANDY + , m_menu_channel_open_tab("Open in New _Tab", true) + , m_menu_dm_open_tab("Open in New _Tab", true) +#endif +#ifdef WITH_VOICE + , m_menu_voice_channel_join("_Join", true) + , m_menu_voice_channel_disconnect("_Disconnect", true) +#endif + , m_menu_dm_copy_id("_Copy ID", true) + , m_menu_dm_close("") // changes depending on if group or not +#ifdef WITH_VOICE + , m_menu_dm_join_voice("Join _Voice", true) + , m_menu_dm_disconnect_voice("_Disconnect Voice", true) +#endif + , 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) + , m_menu_thread_mark_as_read("Mark as _Read", true) { + get_style_context()->add_class("channel-list"); + + // Filter iters + const auto cb = [this](const Gtk::TreeModel::Path &path, Gtk::TreeViewColumn *column) { + auto row = *m_filter_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 && type != RenderType::DM) { + 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) { + const auto id = static_cast(row[m_columns.m_id]); + m_signal_action_channel_item_select.emit(id); + Abaddon::Get().GetDiscordClient().MarkChannelAsRead(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_filter_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]) { + const auto filter_path = m_filter_model->convert_child_path_to_path(m_model->get_path(parent)); + m_view.expand_row(filter_path, false); + } + }); + + m_filter_model->set_visible_func([this](const Gtk::TreeModel::const_iterator &iter) -> bool { + if ((*iter)[m_columns.m_type] == RenderType::Guild) { + return (*iter)[m_columns.m_id] == 754921263616753776ULL; + } + return true; + }); + + 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_id(), m_columns.m_id); + column->add_attribute(renderer->property_expanded(), m_columns.m_expanded); + column->add_attribute(renderer->property_nsfw(), m_columns.m_nsfw); + column->add_attribute(renderer->property_color(), m_columns.m_color); + column->add_attribute(renderer->property_voice_state(), m_columns.m_voice_flags); + 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((*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((*m_model->get_iter(m_path_for_menu))[m_columns.m_id])); + }); + m_menu_guild_mark_as_read.signal_activate().connect([this] { + Abaddon::Get().GetDiscordClient().MarkGuildAsRead(static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]), [](...) {}); + }); + m_menu_guild_toggle_mute.signal_activate().connect([this] { + const auto id = static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]); + auto &discord = Abaddon::Get().GetDiscordClient(); + if (discord.IsGuildMuted(id)) + discord.UnmuteGuild(id, NOOP_CALLBACK); + else + discord.MuteGuild(id, NOOP_CALLBACK); + }); + m_menu_guild.append(m_menu_guild_mark_as_read); + m_menu_guild.append(m_menu_guild_settings); + m_menu_guild.append(m_menu_guild_leave); + m_menu_guild.append(m_menu_guild_toggle_mute); + m_menu_guild.append(m_menu_guild_copy_id); + 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_toggle_mute.signal_activate().connect([this] { + const auto id = static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]); + auto &discord = Abaddon::Get().GetDiscordClient(); + if (discord.IsChannelMuted(id)) + discord.UnmuteChannel(id, NOOP_CALLBACK); + else + discord.MuteChannel(id, NOOP_CALLBACK); + }); + m_menu_category.append(m_menu_category_toggle_mute); + 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_mark_as_read.signal_activate().connect([this] { + Abaddon::Get().GetDiscordClient().MarkChannelAsRead(static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]), [](...) {}); + }); + m_menu_channel_toggle_mute.signal_activate().connect([this] { + const auto id = static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]); + auto &discord = Abaddon::Get().GetDiscordClient(); + if (discord.IsChannelMuted(id)) + discord.UnmuteChannel(id, NOOP_CALLBACK); + else + discord.MuteChannel(id, NOOP_CALLBACK); + }); + +#ifdef WITH_LIBHANDY + m_menu_channel_open_tab.signal_activate().connect([this] { + const auto id = static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]); + m_signal_action_open_new_tab.emit(id); + }); + m_menu_channel.append(m_menu_channel_open_tab); +#endif + + m_menu_channel.append(m_menu_channel_mark_as_read); + m_menu_channel.append(m_menu_channel_toggle_mute); + m_menu_channel.append(m_menu_channel_copy_id); + m_menu_channel.show_all(); + +#ifdef WITH_VOICE + m_menu_voice_channel_join.signal_activate().connect([this]() { + const auto id = static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]); + m_signal_action_join_voice_channel.emit(id); + }); + + m_menu_voice_channel_disconnect.signal_activate().connect([this]() { + m_signal_action_disconnect_voice.emit(); + }); + + m_menu_voice_channel.append(m_menu_voice_channel_join); + m_menu_voice_channel.append(m_menu_voice_channel_disconnect); + m_menu_voice_channel.show_all(); +#endif + + 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((*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_toggle_mute.signal_activate().connect([this] { + const auto id = static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]); + auto &discord = Abaddon::Get().GetDiscordClient(); + if (discord.IsChannelMuted(id)) + discord.UnmuteChannel(id, NOOP_CALLBACK); + else + discord.MuteChannel(id, NOOP_CALLBACK); + }); +#ifdef WITH_LIBHANDY + m_menu_dm_open_tab.signal_activate().connect([this] { + const auto id = static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]); + m_signal_action_open_new_tab.emit(id); + }); + m_menu_dm.append(m_menu_dm_open_tab); +#endif + m_menu_dm.append(m_menu_dm_toggle_mute); + m_menu_dm.append(m_menu_dm_close); +#ifdef WITH_VOICE + m_menu_dm_join_voice.signal_activate().connect([this]() { + const auto id = static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]); + m_signal_action_join_voice_channel.emit(id); + }); + m_menu_dm_disconnect_voice.signal_activate().connect([this]() { + m_signal_action_disconnect_voice.emit(); + }); + m_menu_dm.append(m_menu_dm_join_voice); + m_menu_dm.append(m_menu_dm_disconnect_voice); +#endif + m_menu_dm.append(m_menu_dm_copy_id); + 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((*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((*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((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]), [](...) {}); + }); + m_menu_thread_mark_as_read.signal_activate().connect([this] { + Abaddon::Get().GetDiscordClient().MarkChannelAsRead(static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]), NOOP_CALLBACK); + }); + m_menu_thread_toggle_mute.signal_activate().connect([this] { + const auto id = static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]); + auto &discord = Abaddon::Get().GetDiscordClient(); + if (discord.IsChannelMuted(id)) + discord.UnmuteThread(id, NOOP_CALLBACK); + else + discord.MuteThread(id, NOOP_CALLBACK); + }); + m_menu_thread.append(m_menu_thread_mark_as_read); + m_menu_thread.append(m_menu_thread_toggle_mute); + 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.append(m_menu_thread_copy_id); + m_menu_thread.show_all(); + + 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)); + discord.signal_message_ack().connect(sigc::mem_fun(*this, &ChannelList::OnMessageAck)); + discord.signal_channel_muted().connect(sigc::mem_fun(*this, &ChannelList::OnChannelMute)); + discord.signal_channel_unmuted().connect(sigc::mem_fun(*this, &ChannelList::OnChannelUnmute)); + discord.signal_guild_muted().connect(sigc::mem_fun(*this, &ChannelList::OnGuildMute)); + discord.signal_guild_unmuted().connect(sigc::mem_fun(*this, &ChannelList::OnGuildUnmute)); + +#if WITH_VOICE + discord.signal_voice_user_connect().connect(sigc::mem_fun(*this, &ChannelList::OnVoiceUserConnect)); + discord.signal_voice_user_disconnect().connect(sigc::mem_fun(*this, &ChannelList::OnVoiceUserDisconnect)); + discord.signal_voice_state_set().connect(sigc::mem_fun(*this, &ChannelList::OnVoiceStateSet)); +#endif +} + +void ChannelList::UsePanedHack(Gtk::Paned &paned) { + paned.property_position().signal_changed().connect(sigc::mem_fun(*this, &ChannelList::OnPanedPositionChanged)); +} + +void ChannelList::OnPanedPositionChanged() { + m_view.queue_draw(); +} + +void ChannelList::UpdateListing() { + m_updating_listing = true; + + m_model->clear(); + + auto &discord = Abaddon::Get().GetDiscordClient(); + + /* + guild_folders looks something like this + "guild_folders": [ + { + "color": null, + "guild_ids": [ + "8009060___________" + ], + "id": null, + "name": null + }, + { + "color": null, + "guild_ids": [ + "99615594__________", + "86132141__________", + "35450138__________", + "83714048__________" + ], + "id": 2853066769, + "name": null + } + ] + + so if id != null then its a folder (they can have single entries) + */ + + int sort_value = 0; + + const auto folders = discord.GetUserSettings().GuildFolders; + const auto guild_ids = discord.GetUserSortedGuilds(); + + // user_settings.guild_folders may not contain every guild the user is in + // this seems to be the case if you organize your guilds and join a server without further organization + // so add guilds not present in guild_folders by descending id order first + + std::set foldered_guilds; + for (const auto &group : folders) { + foldered_guilds.insert(group.GuildIDs.begin(), group.GuildIDs.end()); + } + + for (auto iter = guild_ids.rbegin(); iter != guild_ids.rend(); iter++) { + if (foldered_guilds.find(*iter) == foldered_guilds.end()) { + const auto guild = discord.GetGuild(*iter); + if (!guild.has_value()) continue; + auto tree_iter = AddGuild(*guild, m_model->children()); + if (tree_iter) (*tree_iter)[m_columns.m_sort] = sort_value++; + } + } + + // then whatever is in folders + + for (const auto &group : folders) { + auto iter = AddFolder(group); + if (iter) (*iter)[m_columns.m_sort] = sort_value++; + } + + m_updating_listing = false; + + AddPrivateChannels(); +} + +// TODO update for folders +void ChannelList::UpdateNewGuild(const GuildData &guild) { + AddGuild(guild, m_model->children()); + // 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 = GetIteratorForRowFromID(id); + if (!iter) return; + m_model->erase(iter); +} + +void ChannelList::UpdateChannel(Snowflake id) { + auto iter = GetIteratorForRowFromID(id); + auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id); + if (!iter || !channel.has_value()) return; + if (channel->Type == ChannelType::GUILD_CATEGORY) return UpdateChannelCategory(*channel); + if (!channel->IsText()) 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 = GetIteratorForRowFromID(*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 = GetIteratorForRowFromID(*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; + + (*iter)[m_columns.m_name] = "" + Glib::Markup::escape_text(guild->Name) + ""; + (*iter)[m_columns.m_icon] = img.GetPlaceholder(GuildIconSize); + if (Abaddon::Get().GetSettings().ShowAnimations && guild->HasAnimatedIcon()) { + const auto cb = [this, id](const Glib::RefPtr &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 &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 (GetIteratorForRowFromID(id)) return; + const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id); + if (!channel.has_value()) return; + const auto parent = GetIteratorForRowFromID(*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 = GetIteratorForRowFromID(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 threads; + auto guild_iter = GetIteratorForGuildFromID(data.GuildID); + if (!guild_iter) return; + + std::queue 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((*item)[m_columns.m_id])); + for (const 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 = GetIteratorForRowFromID(thread_id); + m_model->erase(iter); + } + } + + // delete all archived threads + for (auto thread : data.Threads) { + if (thread.ThreadMetadata->IsArchived) { + if (auto iter = GetIteratorForRowFromID(thread.ID)) + m_model->erase(iter); + } + } +} + +#ifdef WITH_VOICE +void ChannelList::OnVoiceUserConnect(Snowflake user_id, Snowflake channel_id) { + auto parent_iter = GetIteratorForRowFromIDOfType(channel_id, RenderType::VoiceChannel); + if (!parent_iter) parent_iter = GetIteratorForRowFromIDOfType(channel_id, RenderType::DM); + if (!parent_iter) return; + const auto user = Abaddon::Get().GetDiscordClient().GetUser(user_id); + if (!user.has_value()) return; + + CreateVoiceParticipantRow(*user, parent_iter->children()); +} + +void ChannelList::OnVoiceUserDisconnect(Snowflake user_id, Snowflake channel_id) { + if (auto iter = GetIteratorForRowFromIDOfType(user_id, RenderType::VoiceParticipant)) { + m_model->erase(iter); + } +} + +void ChannelList::OnVoiceStateSet(Snowflake user_id, Snowflake channel_id, VoiceStateFlags flags) { + if (auto iter = GetIteratorForRowFromIDOfType(user_id, RenderType::VoiceParticipant)) { + (*iter)[m_columns.m_voice_flags] = flags; + } +} +#endif + +void ChannelList::DeleteThreadRow(Snowflake id) { + auto iter = GetIteratorForRowFromID(id); + if (iter) + m_model->erase(iter); +} + +void ChannelList::OnChannelMute(Snowflake id) { + if (auto iter = GetIteratorForRowFromID(id)) + m_model->row_changed(m_model->get_path(iter), iter); +} + +void ChannelList::OnChannelUnmute(Snowflake id) { + if (auto iter = GetIteratorForRowFromID(id)) + m_model->row_changed(m_model->get_path(iter), iter); +} + +void ChannelList::OnGuildMute(Snowflake id) { + if (auto iter = GetIteratorForGuildFromID(id)) + m_model->row_changed(m_model->get_path(iter), iter); +} + +void ChannelList::OnGuildUnmute(Snowflake id) { + if (auto iter = GetIteratorForGuildFromID(id)) + m_model->row_changed(m_model->get_path(iter), 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, bool expand_to) { + // mark channel as read when switching off + if (m_active_channel.IsValid()) + Abaddon::Get().GetDiscordClient().MarkChannelAsRead(m_active_channel, [](...) {}); + + m_active_channel = id; + + if (m_temporary_thread_row) { + const auto thread_id = static_cast((*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 = GetIteratorForRowFromID(id); + if (channel_iter) { + if (expand_to) { + m_view.expand_to_path(m_filter_model->convert_child_path_to_path(m_model->get_path(channel_iter))); + } + m_view.get_selection()->select(m_filter_model->convert_child_iter_to_iter(channel_iter)); + } else { + m_view.get_selection()->unselect_all(); + const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id); + if (!channel.has_value() || !channel->IsThread()) return; + auto parent_iter = GetIteratorForRowFromID(*channel->ParentID); + if (!parent_iter) return; + m_temporary_thread_row = CreateThreadRow(parent_iter->children(), *channel); + m_view.get_selection()->select(m_filter_model->convert_child_iter_to_iter(m_temporary_thread_row)); + } +} + +void ChannelList::UseExpansionState(const ExpansionStateRoot &root) { + m_tmp_row_map.clear(); +} + +ExpansionStateRoot ChannelList::GetExpansionState() const { + ExpansionStateRoot r; + + return r; +} + +Gtk::TreeModel::iterator ChannelList::AddFolder(const UserSettingsGuildFoldersEntry &folder) { + if (!folder.ID.has_value()) { + // just a guild + if (!folder.GuildIDs.empty()) { + const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(folder.GuildIDs[0]); + if (guild.has_value()) { + return AddGuild(*guild, m_model->children()); + } + } + } else { + auto folder_row = *m_model->append(); + folder_row[m_columns.m_type] = RenderType::Folder; + folder_row[m_columns.m_id] = *folder.ID; + m_tmp_row_map[*folder.ID] = folder_row; + if (folder.Name.has_value()) { + folder_row[m_columns.m_name] = Glib::Markup::escape_text(*folder.Name); + } else { + folder_row[m_columns.m_name] = "Folder"; + } + if (folder.Color.has_value()) { + folder_row[m_columns.m_color] = IntToRGBA(*folder.Color); + } + + int sort_value = 0; + for (const auto &guild_id : folder.GuildIDs) { + const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(guild_id); + if (guild.has_value()) { + auto guild_row = AddGuild(*guild, folder_row->children()); + (*guild_row)[m_columns.m_sort] = sort_value++; + } + } + + return folder_row; + } + + return {}; +} + +Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild, const Gtk::TreeNodeChildren &root) { + auto &discord = Abaddon::Get().GetDiscordClient(); + auto &img = Abaddon::Get().GetImageManager(); + + auto guild_row = *m_model->append(root); + guild_row[m_columns.m_type] = RenderType::Guild; + guild_row[m_columns.m_id] = guild.ID; + guild_row[m_columns.m_name] = "" + Glib::Markup::escape_text(guild.Name) + ""; + guild_row[m_columns.m_icon] = img.GetPlaceholder(GuildIconSize); + m_tmp_guild_row_map[guild.ID] = guild_row; + + if (Abaddon::Get().GetSettings().ShowAnimations && guild.HasAnimatedIcon()) { + const auto cb = [this, id = guild.ID](const Glib::RefPtr &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 &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 orphan_channels; + std::map> categories; + + for (const auto &channel_ : *guild.Channels) { + const auto channel = discord.GetChannel(channel_.ID); + if (!channel.has_value()) continue; +#ifdef WITH_VOICE + if (channel->Type == ChannelType::GUILD_TEXT || channel->Type == ChannelType::GUILD_NEWS || channel->Type == ChannelType::GUILD_VOICE) { +#else + if (channel->Type == ChannelType::GUILD_TEXT || channel->Type == ChannelType::GUILD_NEWS) { +#endif + 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> 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, const 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) + m_tmp_row_map[thread.ID] = CreateThreadRow(row.children(), thread); + }; + +#ifdef WITH_VOICE + auto add_voice_participants = [this, &discord](const ChannelData &channel, const Gtk::TreeNodeChildren &root) { + for (auto user_id : discord.GetUsersInVoiceChannel(channel.ID)) { + if (const auto user = discord.GetUser(user_id); user.has_value()) { + CreateVoiceParticipantRow(*user, root); + } + } + }; +#endif + + for (const auto &channel : orphan_channels) { + auto channel_row = *m_model->append(guild_row.children()); + if (IsTextChannel(channel.Type)) { + channel_row[m_columns.m_type] = RenderType::TextChannel; + channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name); + } +#ifdef WITH_VOICE + else { + channel_row[m_columns.m_type] = RenderType::VoiceChannel; + channel_row[m_columns.m_name] = Glib::Markup::escape_text(*channel.Name); + add_voice_participants(channel, channel_row->children()); + } +#endif + channel_row[m_columns.m_id] = channel.ID; + channel_row[m_columns.m_sort] = *channel.Position + OrphanChannelSortOffset; + channel_row[m_columns.m_nsfw] = channel.NSFW(); + add_threads(channel, channel_row); + m_tmp_row_map[channel.ID] = 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_tmp_row_map[category_id] = cat_row; + // 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()); + if (IsTextChannel(channel.Type)) { + channel_row[m_columns.m_type] = RenderType::TextChannel; + channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name); + } +#ifdef WITH_VOICE + else { + channel_row[m_columns.m_type] = RenderType::VoiceChannel; + channel_row[m_columns.m_name] = Glib::Markup::escape_text(*channel.Name); + add_voice_participants(channel, channel_row->children()); + } +#endif + channel_row[m_columns.m_id] = channel.ID; + channel_row[m_columns.m_sort] = *channel.Position; + channel_row[m_columns.m_nsfw] = channel.NSFW(); + add_threads(channel, channel_row); + m_tmp_row_map[channel.ID] = 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] = static_cast(channel.ID); + thread_row[m_columns.m_nsfw] = false; + + return thread_iter; +} + +#ifdef WITH_VOICE +Gtk::TreeModel::iterator ChannelList::CreateVoiceParticipantRow(const UserData &user, const Gtk::TreeNodeChildren &parent) { + auto row = *m_model->append(parent); + row[m_columns.m_type] = RenderType::VoiceParticipant; + row[m_columns.m_id] = user.ID; + row[m_columns.m_name] = user.GetDisplayNameEscaped(); + + const auto voice_state = Abaddon::Get().GetDiscordClient().GetVoiceState(user.ID); + if (voice_state.has_value()) { + row[m_columns.m_voice_flags] = voice_state->second; + } + + auto &img = Abaddon::Get().GetImageManager(); + row[m_columns.m_icon] = img.GetPlaceholder(VoiceParticipantIconSize); + const auto cb = [this, user_id = user.ID](const Glib::RefPtr &pb) { + auto iter = GetIteratorForRowFromIDOfType(user_id, RenderType::VoiceParticipant); + if (iter) (*iter)[m_columns.m_icon] = pb->scale_simple(VoiceParticipantIconSize, VoiceParticipantIconSize, Gdk::INTERP_BILINEAR); + }; + img.LoadFromURL(user.GetAvatarURL("png", "32"), sigc::track_obj(cb, *this)); + + return row; +} +#endif + +void ChannelList::UpdateChannelCategory(const ChannelData &channel) { + auto iter = GetIteratorForRowFromID(channel.ID); + if (!iter) return; + + (*iter)[m_columns.m_sort] = *channel.Position; + (*iter)[m_columns.m_name] = Glib::Markup::escape_text(*channel.Name); +} + +// todo this all needs refactoring for shooore +Gtk::TreeModel::iterator ChannelList::GetIteratorForTopLevelFromID(Snowflake id) { + for (const auto &child : m_model->children()) { + if ((child[m_columns.m_type] == RenderType::Guild || child[m_columns.m_type] == RenderType::Folder) && child[m_columns.m_id] == id) { + return child; + } else if (child[m_columns.m_type] == RenderType::Folder) { + for (const auto &folder_child : child->children()) { + if (folder_child[m_columns.m_id] == id) { + return folder_child; + } + } + } + } + return {}; +} + +Gtk::TreeModel::iterator ChannelList::GetIteratorForGuildFromID(Snowflake id) { + for (const auto &child : m_model->children()) { + if (child[m_columns.m_type] == RenderType::Guild && child[m_columns.m_id] == id) { + return child; + } else if (child[m_columns.m_type] == RenderType::Folder) { + for (const auto &folder_child : child->children()) { + if (folder_child[m_columns.m_id] == id) { + return folder_child; + } + } + } + } + return {}; +} + +Gtk::TreeModel::iterator ChannelList::GetIteratorForRowFromID(Snowflake id) { + std::queue 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 && (*item)[m_columns.m_type] != RenderType::Guild) return item; + for (const auto &child : item->children()) + queue.push(child); + queue.pop(); + } + + return {}; +} + +Gtk::TreeModel::iterator ChannelList::GetIteratorForRowFromIDOfType(Snowflake id, RenderType type) { + std::queue 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_type] == type && (*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) const { + (*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_filter_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 &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_filter_model->get_path(row); + } + } + + auto type = (*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] = "Direct Messages"; + 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 top_recipient; + const auto recipients = dm->GetDMRecipients(); + if (!recipients.empty()) + 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_name] = Glib::Markup::escape_text(dm->GetDisplayName()); + row[m_columns.m_sort] = static_cast(-(dm->LastMessageID.has_value() ? *dm->LastMessageID : dm_id)); + row[m_columns.m_icon] = img.GetPlaceholder(DMIconSize); + row[m_columns.m_expanded] = true; + +#ifdef WITH_VOICE + for (auto user_id : discord.GetUsersInVoiceChannel(dm_id)) { + if (const auto user = discord.GetUser(user_id); user.has_value()) { + CreateVoiceParticipantRow(*user, row->children()); + } + } +#endif + + SetDMChannelIcon(iter, *dm); + } +} + +void ChannelList::UpdateCreateDMChannel(const ChannelData &dm) { + auto header_row = m_model->get_iter(m_dm_header); + auto &img = Abaddon::Get().GetImageManager(); + + 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_name] = Glib::Markup::escape_text(dm.GetDisplayName()); + row[m_columns.m_sort] = static_cast(-(dm.LastMessageID.has_value() ? *dm.LastMessageID : dm.ID)); + row[m_columns.m_icon] = img.GetPlaceholder(DMIconSize); + + SetDMChannelIcon(iter, dm); +} + +void ChannelList::SetDMChannelIcon(Gtk::TreeIter iter, const ChannelData &dm) { + auto &img = Abaddon::Get().GetImageManager(); + + std::optional top_recipient; + const auto recipients = dm.GetDMRecipients(); + if (!recipients.empty()) + top_recipient = recipients[0]; + + if (dm.HasIcon()) { + const auto cb = [this, iter](const Glib::RefPtr &pb) { + if (iter) + (*iter)[m_columns.m_icon] = pb->scale_simple(DMIconSize, DMIconSize, Gdk::INTERP_BILINEAR); + }; + img.LoadFromURL(dm.GetIconURL(), sigc::track_obj(cb, *this)); + } else if (dm.Type == ChannelType::DM && top_recipient.has_value()) { + const auto cb = [this, iter](const Glib::RefPtr &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)); + } else { // GROUP_DM + std::string hash; + switch (dm.ID.GetUnixMilliseconds() % 8) { + case 0: + hash = "ee9275c5a437f7dc7f9430ba95f12ebd"; + break; + case 1: + hash = "9baf45aac2a0ec2e2dab288333acb9d9"; + break; + case 2: + hash = "7ba11ffb1900fa2b088cb31324242047"; + break; + case 3: + hash = "f90fca70610c4898bc57b58bce92f587"; + break; + case 4: + hash = "e2779af34b8d9126b77420e5f09213ce"; + break; + case 5: + hash = "c6851bd0b03f1cca5a8c1e720ea6ea17"; + break; + case 6: + hash = "f7e38ac976a2a696161c923502a8345b"; + break; + case 7: + default: + hash = "3cb840d03313467838d658bbec801fcd"; + break; + } + const auto cb = [this, iter](const Glib::RefPtr &pb) { + if (iter) + (*iter)[m_columns.m_icon] = pb->scale_simple(DMIconSize, DMIconSize, Gdk::INTERP_BILINEAR); + }; + img.LoadFromURL("https://discord.com/assets/" + hash + ".png", sigc::track_obj(cb, *this)); + } +} + +void ChannelList::RedrawUnreadIndicatorsForChannel(const ChannelData &channel) { + if (channel.GuildID.has_value()) { + auto iter = GetIteratorForGuildFromID(*channel.GuildID); + if (iter) m_model->row_changed(m_model->get_path(iter), iter); + } + if (channel.ParentID.has_value()) { + auto iter = GetIteratorForRowFromIDOfType(*channel.ParentID, RenderType::Category); + if (iter) m_model->row_changed(m_model->get_path(iter), iter); + } +} + +void ChannelList::OnMessageAck(const MessageAckData &data) { + // trick renderer into redrawing + m_model->row_changed(Gtk::TreeModel::Path("0"), m_model->get_iter("0")); // 0 is always path for dm header + auto iter = GetIteratorForRowFromID(data.ChannelID); + if (iter) m_model->row_changed(m_model->get_path(iter), iter); + auto channel = Abaddon::Get().GetDiscordClient().GetChannel(data.ChannelID); + if (channel.has_value()) { + RedrawUnreadIndicatorsForChannel(*channel); + } +} + +void ChannelList::OnMessageCreate(const Message &msg) { + auto iter = GetIteratorForRowFromID(msg.ChannelID); + if (iter) m_model->row_changed(m_model->get_path(iter), iter); // redraw + const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(msg.ChannelID); + if (!channel.has_value()) return; + if (channel->Type == ChannelType::DM || channel->Type == ChannelType::GROUP_DM) { + if (iter) + (*iter)[m_columns.m_sort] = static_cast(-msg.ID); + } + RedrawUnreadIndicatorsForChannel(*channel); +} + +bool ChannelList::OnButtonPressEvent(GdkEventButton *ev) { + if (ev->button == GDK_BUTTON_SECONDARY && ev->type == GDK_BUTTON_PRESS) { + if (m_view.get_path_at_pos(static_cast(ev->x), static_cast(ev->y), m_path_for_menu)) { + auto row = (*m_filter_model->get_iter(m_path_for_menu)); + switch (static_cast(row[m_columns.m_type])) { + case RenderType::Guild: + OnGuildSubmenuPopup(); + m_menu_guild.popup_at_pointer(reinterpret_cast(ev)); + break; + case RenderType::Category: + OnCategorySubmenuPopup(); + m_menu_category.popup_at_pointer(reinterpret_cast(ev)); + break; + case RenderType::TextChannel: + OnChannelSubmenuPopup(); + m_menu_channel.popup_at_pointer(reinterpret_cast(ev)); + break; +#ifdef WITH_VOICE + case RenderType::VoiceChannel: + OnVoiceChannelSubmenuPopup(); + m_menu_voice_channel.popup_at_pointer(reinterpret_cast(ev)); + break; +#endif + case RenderType::DM: { + OnDMSubmenuPopup(); + const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(static_cast(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(ev)); + } break; + case RenderType::Thread: { + OnThreadSubmenuPopup(); + m_menu_thread.popup_at_pointer(reinterpret_cast(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((*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); + M(m_color); +#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(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::OnGuildSubmenuPopup() { + const auto iter = m_model->get_iter(m_path_for_menu); + if (!iter) return; + const auto id = static_cast((*iter)[m_columns.m_id]); + auto &discord = Abaddon::Get().GetDiscordClient(); + if (discord.IsGuildMuted(id)) + m_menu_guild_toggle_mute.set_label("Unmute"); + else + m_menu_guild_toggle_mute.set_label("Mute"); + + const auto guild = discord.GetGuild(id); + const auto self_id = discord.GetUserData().ID; + m_menu_guild_leave.set_sensitive(!(guild.has_value() && guild->OwnerID == self_id)); +} + +void ChannelList::OnCategorySubmenuPopup() { + const auto iter = m_model->get_iter(m_path_for_menu); + if (!iter) return; + const auto id = static_cast((*iter)[m_columns.m_id]); + if (Abaddon::Get().GetDiscordClient().IsChannelMuted(id)) + m_menu_category_toggle_mute.set_label("Unmute"); + else + m_menu_category_toggle_mute.set_label("Mute"); +} + +void ChannelList::OnChannelSubmenuPopup() { + const auto iter = m_model->get_iter(m_path_for_menu); + if (!iter) return; + const auto id = static_cast((*iter)[m_columns.m_id]); + auto &discord = Abaddon::Get().GetDiscordClient(); +#ifdef WITH_LIBHANDY + const auto perms = discord.HasChannelPermission(discord.GetUserData().ID, id, Permission::VIEW_CHANNEL); + m_menu_channel_open_tab.set_sensitive(perms); +#endif + if (discord.IsChannelMuted(id)) + m_menu_channel_toggle_mute.set_label("Unmute"); + else + m_menu_channel_toggle_mute.set_label("Mute"); +} + +#ifdef WITH_VOICE +void ChannelList::OnVoiceChannelSubmenuPopup() { + const auto iter = m_model->get_iter(m_path_for_menu); + if (!iter) return; + const auto id = static_cast((*iter)[m_columns.m_id]); + auto &discord = Abaddon::Get().GetDiscordClient(); + if (discord.IsVoiceConnected() || discord.IsVoiceConnecting()) { + m_menu_voice_channel_join.set_sensitive(false); + m_menu_voice_channel_disconnect.set_sensitive(discord.GetVoiceChannelID() == id); + } else { + m_menu_voice_channel_join.set_sensitive(true); + m_menu_voice_channel_disconnect.set_sensitive(false); + } +} +#endif + +void ChannelList::OnDMSubmenuPopup() { + auto iter = m_model->get_iter(m_path_for_menu); + if (!iter) return; + const auto id = static_cast((*iter)[m_columns.m_id]); + auto &discord = Abaddon::Get().GetDiscordClient(); + if (discord.IsChannelMuted(id)) + m_menu_dm_toggle_mute.set_label("Unmute"); + else + m_menu_dm_toggle_mute.set_label("Mute"); + +#ifdef WITH_VOICE + if (discord.IsVoiceConnected() || discord.IsVoiceConnecting()) { + m_menu_dm_join_voice.set_sensitive(false); + m_menu_dm_disconnect_voice.set_sensitive(discord.GetVoiceChannelID() == id); + } else { + m_menu_dm_join_voice.set_sensitive(true); + m_menu_dm_disconnect_voice.set_sensitive(false); + } +#endif +} + +void ChannelList::OnThreadSubmenuPopup() { + 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; + const auto id = static_cast((*iter)[m_columns.m_id]); + + if (discord.IsChannelMuted(id)) + m_menu_thread_toggle_mute.set_label("Unmute"); + else + m_menu_thread_toggle_mute.set_label("Mute"); + + auto channel = discord.GetChannel(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; +} + +#ifdef WITH_LIBHANDY +ChannelList::type_signal_action_open_new_tab ChannelList::signal_action_open_new_tab() { + return m_signal_action_open_new_tab; +} +#endif + +#ifdef WITH_VOICE +ChannelList::type_signal_action_join_voice_channel ChannelList::signal_action_join_voice_channel() { + return m_signal_action_join_voice_channel; +} + +ChannelList::type_signal_action_disconnect_voice ChannelList::signal_action_disconnect_voice() { + return m_signal_action_disconnect_voice; +} +#endif + +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); + add(m_color); + add(m_voice_flags); +} diff --git a/src/components/channellist/channellist.hpp b/src/components/channellist/channellist.hpp new file mode 100644 index 0000000..df7dbac --- /dev/null +++ b/src/components/channellist/channellist.hpp @@ -0,0 +1,231 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "discord/discord.hpp" +#include "state.hpp" +#include "cellrendererchannels.hpp" + +constexpr static int GuildIconSize = 24; +constexpr static int DMIconSize = 20; +constexpr static int VoiceParticipantIconSize = 18; +constexpr static int OrphanChannelSortOffset = -100; // forces orphan channels to the top of the list + +class ChannelList : public Gtk::ScrolledWindow { +public: + ChannelList(); + + void UpdateListing(); + void SetActiveChannel(Snowflake id, bool expand_to); + + // channel list should be populated when this is called + void UseExpansionState(const ExpansionStateRoot &state); + ExpansionStateRoot GetExpansionState() const; + + void UsePanedHack(Gtk::Paned &paned); + +protected: + void OnPanedPositionChanged(); + + void UpdateNewGuild(const GuildData &guild); + void UpdateRemoveGuild(Snowflake id); + void UpdateRemoveChannel(Snowflake id); + void UpdateChannel(Snowflake id); + void UpdateCreateChannel(const ChannelData &channel); + void UpdateGuild(Snowflake id); + void DeleteThreadRow(Snowflake id); + void OnChannelMute(Snowflake id); + void OnChannelUnmute(Snowflake id); + void OnGuildMute(Snowflake id); + void OnGuildUnmute(Snowflake id); + + void OnThreadJoined(Snowflake id); + void OnThreadRemoved(Snowflake id); + void OnThreadDelete(const ThreadDeleteData &data); + void OnThreadUpdate(const ThreadUpdateData &data); + void OnThreadListSync(const ThreadListSyncData &data); + + void OnVoiceUserConnect(Snowflake user_id, Snowflake channel_id); + void OnVoiceUserDisconnect(Snowflake user_id, Snowflake channel_id); + void OnVoiceStateSet(Snowflake user_id, Snowflake channel_id, VoiceStateFlags flags); + + Gtk::TreeView m_view; + + class ModelColumns : public Gtk::TreeModel::ColumnRecord { + public: + ModelColumns(); + + Gtk::TreeModelColumn m_type; + Gtk::TreeModelColumn m_id; + Gtk::TreeModelColumn m_name; + Gtk::TreeModelColumn> m_icon; + Gtk::TreeModelColumn> m_icon_anim; + Gtk::TreeModelColumn m_sort; + Gtk::TreeModelColumn m_nsfw; + Gtk::TreeModelColumn> m_color; // for folders right now + Gtk::TreeModelColumn m_voice_flags; + // Gtk::CellRenderer's property_is_expanded only works how i want it to if it has children + // because otherwise it doesnt count as an "expander" (property_is_expander) + // so this solution will have to do which i hate but the alternative is adding invisible children + // to all categories without children and having a filter model but that sounds worse + // of course its a lot better than the absolute travesty i had before + Gtk::TreeModelColumn m_expanded; + }; + + ModelColumns m_columns; + Glib::RefPtr m_model; + Glib::RefPtr m_filter_model; + + Gtk::TreeModel::iterator AddFolder(const UserSettingsGuildFoldersEntry &folder); + Gtk::TreeModel::iterator AddGuild(const GuildData &guild, const Gtk::TreeNodeChildren &root); + Gtk::TreeModel::iterator UpdateCreateChannelCategory(const ChannelData &channel); + Gtk::TreeModel::iterator CreateThreadRow(const Gtk::TreeNodeChildren &children, const ChannelData &channel); + +#ifdef WITH_VOICE + Gtk::TreeModel::iterator CreateVoiceParticipantRow(const UserData &user, const Gtk::TreeNodeChildren &parent); +#endif + + void UpdateChannelCategory(const ChannelData &channel); + + // separation necessary because a channel and guild can share the same id + Gtk::TreeModel::iterator GetIteratorForTopLevelFromID(Snowflake id); + Gtk::TreeModel::iterator GetIteratorForGuildFromID(Snowflake id); + Gtk::TreeModel::iterator GetIteratorForRowFromID(Snowflake id); + Gtk::TreeModel::iterator GetIteratorForRowFromIDOfType(Snowflake id, RenderType type); + + bool IsTextChannel(ChannelType type); + + void OnRowCollapsed(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path) const; + void OnRowExpanded(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path); + bool SelectionFunc(const Glib::RefPtr &model, const Gtk::TreeModel::Path &path, bool is_currently_selected); + bool OnButtonPressEvent(GdkEventButton *ev); + + void MoveRow(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::iterator &new_parent); + + Gtk::TreeModel::Path m_last_selected; + Gtk::TreeModel::Path m_dm_header; + + void AddPrivateChannels(); + void UpdateCreateDMChannel(const ChannelData &channel); + void SetDMChannelIcon(Gtk::TreeIter iter, const ChannelData &dm); + + void RedrawUnreadIndicatorsForChannel(const ChannelData &channel); + void OnMessageAck(const MessageAckData &data); + void OnMessageCreate(const Message &msg); + + Gtk::TreeModel::Path m_path_for_menu; + + // cant be recovered through selection + Gtk::TreeModel::iterator m_temporary_thread_row; + + Gtk::Menu m_menu_guild; + Gtk::MenuItem m_menu_guild_copy_id; + Gtk::MenuItem m_menu_guild_settings; + Gtk::MenuItem m_menu_guild_leave; + Gtk::MenuItem m_menu_guild_mark_as_read; + Gtk::MenuItem m_menu_guild_toggle_mute; + + Gtk::Menu m_menu_category; + Gtk::MenuItem m_menu_category_copy_id; + Gtk::MenuItem m_menu_category_toggle_mute; + + Gtk::Menu m_menu_channel; + Gtk::MenuItem m_menu_channel_copy_id; + Gtk::MenuItem m_menu_channel_mark_as_read; + Gtk::MenuItem m_menu_channel_toggle_mute; + +#ifdef WITH_LIBHANDY + Gtk::MenuItem m_menu_channel_open_tab; +#endif + +#ifdef WITH_VOICE + Gtk::Menu m_menu_voice_channel; + Gtk::MenuItem m_menu_voice_channel_join; + Gtk::MenuItem m_menu_voice_channel_disconnect; +#endif + + Gtk::Menu m_menu_dm; + Gtk::MenuItem m_menu_dm_copy_id; + Gtk::MenuItem m_menu_dm_close; + Gtk::MenuItem m_menu_dm_toggle_mute; +#ifdef WITH_VOICE + Gtk::MenuItem m_menu_dm_join_voice; + Gtk::MenuItem m_menu_dm_disconnect_voice; +#endif + +#ifdef WITH_LIBHANDY + Gtk::MenuItem m_menu_dm_open_tab; +#endif + + Gtk::Menu m_menu_thread; + Gtk::MenuItem m_menu_thread_copy_id; + Gtk::MenuItem m_menu_thread_leave; + Gtk::MenuItem m_menu_thread_archive; + Gtk::MenuItem m_menu_thread_unarchive; + Gtk::MenuItem m_menu_thread_mark_as_read; + Gtk::MenuItem m_menu_thread_toggle_mute; + + void OnGuildSubmenuPopup(); + void OnCategorySubmenuPopup(); + void OnChannelSubmenuPopup(); + void OnDMSubmenuPopup(); + void OnThreadSubmenuPopup(); + +#ifdef WITH_VOICE + void OnVoiceChannelSubmenuPopup(); +#endif + + bool m_updating_listing = false; + + Snowflake m_active_channel; + + // (GetIteratorForChannelFromID is rather slow) + // only temporary since i dont want to worry about maintaining this map + std::unordered_map m_tmp_row_map; + std::unordered_map m_tmp_guild_row_map; + +public: + using type_signal_action_channel_item_select = sigc::signal; + using type_signal_action_guild_leave = sigc::signal; + using type_signal_action_guild_settings = sigc::signal; + +#ifdef WITH_LIBHANDY + using type_signal_action_open_new_tab = sigc::signal; + type_signal_action_open_new_tab signal_action_open_new_tab(); +#endif + +#ifdef WITH_VOICE + using type_signal_action_join_voice_channel = sigc::signal; + using type_signal_action_disconnect_voice = sigc::signal; + + type_signal_action_join_voice_channel signal_action_join_voice_channel(); + type_signal_action_disconnect_voice signal_action_disconnect_voice(); +#endif + + type_signal_action_channel_item_select signal_action_channel_item_select(); + type_signal_action_guild_leave signal_action_guild_leave(); + type_signal_action_guild_settings signal_action_guild_settings(); + +private: + type_signal_action_channel_item_select m_signal_action_channel_item_select; + type_signal_action_guild_leave m_signal_action_guild_leave; + type_signal_action_guild_settings m_signal_action_guild_settings; + +#ifdef WITH_LIBHANDY + type_signal_action_open_new_tab m_signal_action_open_new_tab; +#endif + +#ifdef WITH_VOICE + type_signal_action_join_voice_channel m_signal_action_join_voice_channel; + type_signal_action_disconnect_voice m_signal_action_disconnect_voice; +#endif +}; diff --git a/src/components/channellist/classic/guildlist.cpp b/src/components/channellist/classic/guildlist.cpp new file mode 100644 index 0000000..0d5737b --- /dev/null +++ b/src/components/channellist/classic/guildlist.cpp @@ -0,0 +1,23 @@ +#include "guildlist.hpp" +#include "guildlistfolderitem.hpp" + +GuildList::GuildList() { + set_selection_mode(Gtk::SELECTION_NONE); + show_all_children(); +} + +void GuildList::AddGuild(Snowflake id) { + const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(id); + if (!guild.has_value()) return; + + auto *item = Gtk::make_managed(*guild); + item->show(); + add(*item); +} + +void GuildList::Clear() { + const auto children = get_children(); + for (auto child : children) { + delete child; + } +} diff --git a/src/components/channellist/classic/guildlist.hpp b/src/components/channellist/classic/guildlist.hpp new file mode 100644 index 0000000..244313f --- /dev/null +++ b/src/components/channellist/classic/guildlist.hpp @@ -0,0 +1,12 @@ +#pragma once +#include +#include "discord/snowflake.hpp" + +class GuildList : public Gtk::ListBox { +public: + GuildList(); + + void AddGuild(Snowflake id); + + void Clear(); +}; diff --git a/src/components/channellist/classic/guildlistfolderitem.cpp b/src/components/channellist/classic/guildlistfolderitem.cpp new file mode 100644 index 0000000..36d5a5a --- /dev/null +++ b/src/components/channellist/classic/guildlistfolderitem.cpp @@ -0,0 +1,18 @@ +#include "guildlistfolderitem.hpp" + +GuildListFolderItem::GuildListFolderItem() { + m_revealer.add(m_box); + m_revealer.set_reveal_child(true); + + m_ev.signal_button_press_event().connect([this](GdkEventButton *event) -> bool { + if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_PRIMARY) { + m_revealer.set_reveal_child(!m_revealer.get_reveal_child()); + } + return false; + }); + + m_ev.add(m_image); + add(m_ev); + add(m_revealer); + show_all_children(); +} diff --git a/src/components/channellist/classic/guildlistfolderitem.hpp b/src/components/channellist/classic/guildlistfolderitem.hpp new file mode 100644 index 0000000..3506969 --- /dev/null +++ b/src/components/channellist/classic/guildlistfolderitem.hpp @@ -0,0 +1,18 @@ +#pragma once +#include +#include +#include +#include + +#include "guildlistguilditem.hpp" + +class GuildListFolderItem : public Gtk::VBox { +public: + GuildListFolderItem(); + +private: + Gtk::EventBox m_ev; + Gtk::Image m_image; + Gtk::Revealer m_revealer; + Gtk::VBox m_box; +}; diff --git a/src/components/channellist/classic/guildlistguilditem.cpp b/src/components/channellist/classic/guildlistguilditem.cpp new file mode 100644 index 0000000..88ed2a9 --- /dev/null +++ b/src/components/channellist/classic/guildlistguilditem.cpp @@ -0,0 +1,29 @@ +#include "guildlistguilditem.hpp" + +GuildListGuildItem::GuildListGuildItem(const GuildData &guild) + : ID(guild.ID) { + m_image.property_pixbuf() = Abaddon::Get().GetImageManager().GetPlaceholder(48); + add(m_image); + show_all_children(); + + signal_button_press_event().connect([this](GdkEventButton *event) -> bool { + if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_PRIMARY) { + printf("Click %llu\n", (uint64_t)ID); + } + return true; + }); + + set_tooltip_text(guild.Name); + + UpdateIcon(); +} + +void GuildListGuildItem::UpdateIcon() { + const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(ID); + if (!guild.has_value()) return; + Abaddon::Get().GetImageManager().LoadFromURL(guild->GetIconURL("png", "64"), sigc::mem_fun(*this, &GuildListGuildItem::OnIconFetched)); +} + +void GuildListGuildItem::OnIconFetched(const Glib::RefPtr &pb) { + m_image.property_pixbuf() = pb->scale_simple(48, 48, Gdk::INTERP_BILINEAR); +} diff --git a/src/components/channellist/classic/guildlistguilditem.hpp b/src/components/channellist/classic/guildlistguilditem.hpp new file mode 100644 index 0000000..3114a05 --- /dev/null +++ b/src/components/channellist/classic/guildlistguilditem.hpp @@ -0,0 +1,17 @@ +#pragma once +#include +#include +#include "discord/guild.hpp" + +class GuildListGuildItem : public Gtk::EventBox { +public: + GuildListGuildItem(const GuildData &guild); + + Snowflake ID; + +private: + void UpdateIcon(); + void OnIconFetched(const Glib::RefPtr &pb); + + Gtk::Image m_image; +}; diff --git a/src/components/channels.cpp b/src/components/channels.cpp deleted file mode 100644 index 9fd4abd..0000000 --- a/src/components/channels.cpp +++ /dev/null @@ -1,1352 +0,0 @@ -#include "channels.hpp" -#include "imgmanager.hpp" -#include "statusindicator.hpp" -#include -#include -#include - -ChannelList::ChannelList() - : Glib::ObjectBase(typeid(ChannelList)) - , 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_guild_mark_as_read("Mark as _Read", true) - , m_menu_category_copy_id("_Copy ID", true) - , m_menu_channel_copy_id("_Copy ID", true) - , m_menu_channel_mark_as_read("Mark as _Read", true) -#ifdef WITH_LIBHANDY - , m_menu_channel_open_tab("Open in New _Tab", true) - , m_menu_dm_open_tab("Open in New _Tab", true) -#endif -#ifdef WITH_VOICE - , m_menu_voice_channel_join("_Join", true) - , m_menu_voice_channel_disconnect("_Disconnect", true) -#endif - , m_menu_dm_copy_id("_Copy ID", true) - , m_menu_dm_close("") // changes depending on if group or not -#ifdef WITH_VOICE - , m_menu_dm_join_voice("Join _Voice", true) - , m_menu_dm_disconnect_voice("_Disconnect Voice", true) -#endif - , 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) - , m_menu_thread_mark_as_read("Mark as _Read", true) { - get_style_context()->add_class("channel-list"); - - // todo: move to method - 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 && type != RenderType::DM) { - 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) { - const auto id = static_cast(row[m_columns.m_id]); - m_signal_action_channel_item_select.emit(id); - Abaddon::Get().GetDiscordClient().MarkChannelAsRead(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_id(), m_columns.m_id); - column->add_attribute(renderer->property_expanded(), m_columns.m_expanded); - column->add_attribute(renderer->property_nsfw(), m_columns.m_nsfw); - column->add_attribute(renderer->property_color(), m_columns.m_color); - column->add_attribute(renderer->property_voice_state(), m_columns.m_voice_flags); - 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((*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((*m_model->get_iter(m_path_for_menu))[m_columns.m_id])); - }); - m_menu_guild_mark_as_read.signal_activate().connect([this] { - Abaddon::Get().GetDiscordClient().MarkGuildAsRead(static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]), [](...) {}); - }); - m_menu_guild_toggle_mute.signal_activate().connect([this] { - const auto id = static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]); - auto &discord = Abaddon::Get().GetDiscordClient(); - if (discord.IsGuildMuted(id)) - discord.UnmuteGuild(id, NOOP_CALLBACK); - else - discord.MuteGuild(id, NOOP_CALLBACK); - }); - m_menu_guild.append(m_menu_guild_mark_as_read); - m_menu_guild.append(m_menu_guild_settings); - m_menu_guild.append(m_menu_guild_leave); - m_menu_guild.append(m_menu_guild_toggle_mute); - m_menu_guild.append(m_menu_guild_copy_id); - 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_toggle_mute.signal_activate().connect([this] { - const auto id = static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]); - auto &discord = Abaddon::Get().GetDiscordClient(); - if (discord.IsChannelMuted(id)) - discord.UnmuteChannel(id, NOOP_CALLBACK); - else - discord.MuteChannel(id, NOOP_CALLBACK); - }); - m_menu_category.append(m_menu_category_toggle_mute); - 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_mark_as_read.signal_activate().connect([this] { - Abaddon::Get().GetDiscordClient().MarkChannelAsRead(static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]), [](...) {}); - }); - m_menu_channel_toggle_mute.signal_activate().connect([this] { - const auto id = static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]); - auto &discord = Abaddon::Get().GetDiscordClient(); - if (discord.IsChannelMuted(id)) - discord.UnmuteChannel(id, NOOP_CALLBACK); - else - discord.MuteChannel(id, NOOP_CALLBACK); - }); - -#ifdef WITH_LIBHANDY - m_menu_channel_open_tab.signal_activate().connect([this] { - const auto id = static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]); - m_signal_action_open_new_tab.emit(id); - }); - m_menu_channel.append(m_menu_channel_open_tab); -#endif - - m_menu_channel.append(m_menu_channel_mark_as_read); - m_menu_channel.append(m_menu_channel_toggle_mute); - m_menu_channel.append(m_menu_channel_copy_id); - m_menu_channel.show_all(); - -#ifdef WITH_VOICE - m_menu_voice_channel_join.signal_activate().connect([this]() { - const auto id = static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]); - m_signal_action_join_voice_channel.emit(id); - }); - - m_menu_voice_channel_disconnect.signal_activate().connect([this]() { - m_signal_action_disconnect_voice.emit(); - }); - - m_menu_voice_channel.append(m_menu_voice_channel_join); - m_menu_voice_channel.append(m_menu_voice_channel_disconnect); - m_menu_voice_channel.show_all(); -#endif - - 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((*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_toggle_mute.signal_activate().connect([this] { - const auto id = static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]); - auto &discord = Abaddon::Get().GetDiscordClient(); - if (discord.IsChannelMuted(id)) - discord.UnmuteChannel(id, NOOP_CALLBACK); - else - discord.MuteChannel(id, NOOP_CALLBACK); - }); -#ifdef WITH_LIBHANDY - m_menu_dm_open_tab.signal_activate().connect([this] { - const auto id = static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]); - m_signal_action_open_new_tab.emit(id); - }); - m_menu_dm.append(m_menu_dm_open_tab); -#endif - m_menu_dm.append(m_menu_dm_toggle_mute); - m_menu_dm.append(m_menu_dm_close); -#ifdef WITH_VOICE - m_menu_dm_join_voice.signal_activate().connect([this]() { - const auto id = static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]); - m_signal_action_join_voice_channel.emit(id); - }); - m_menu_dm_disconnect_voice.signal_activate().connect([this]() { - m_signal_action_disconnect_voice.emit(); - }); - m_menu_dm.append(m_menu_dm_join_voice); - m_menu_dm.append(m_menu_dm_disconnect_voice); -#endif - m_menu_dm.append(m_menu_dm_copy_id); - 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((*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((*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((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]), [](...) {}); - }); - m_menu_thread_mark_as_read.signal_activate().connect([this] { - Abaddon::Get().GetDiscordClient().MarkChannelAsRead(static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]), NOOP_CALLBACK); - }); - m_menu_thread_toggle_mute.signal_activate().connect([this] { - const auto id = static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]); - auto &discord = Abaddon::Get().GetDiscordClient(); - if (discord.IsChannelMuted(id)) - discord.UnmuteThread(id, NOOP_CALLBACK); - else - discord.MuteThread(id, NOOP_CALLBACK); - }); - m_menu_thread.append(m_menu_thread_mark_as_read); - m_menu_thread.append(m_menu_thread_toggle_mute); - 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.append(m_menu_thread_copy_id); - m_menu_thread.show_all(); - - 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)); - discord.signal_message_ack().connect(sigc::mem_fun(*this, &ChannelList::OnMessageAck)); - discord.signal_channel_muted().connect(sigc::mem_fun(*this, &ChannelList::OnChannelMute)); - discord.signal_channel_unmuted().connect(sigc::mem_fun(*this, &ChannelList::OnChannelUnmute)); - discord.signal_guild_muted().connect(sigc::mem_fun(*this, &ChannelList::OnGuildMute)); - discord.signal_guild_unmuted().connect(sigc::mem_fun(*this, &ChannelList::OnGuildUnmute)); - -#if WITH_VOICE - discord.signal_voice_user_connect().connect(sigc::mem_fun(*this, &ChannelList::OnVoiceUserConnect)); - discord.signal_voice_user_disconnect().connect(sigc::mem_fun(*this, &ChannelList::OnVoiceUserDisconnect)); - discord.signal_voice_state_set().connect(sigc::mem_fun(*this, &ChannelList::OnVoiceStateSet)); -#endif -} - -void ChannelList::UsePanedHack(Gtk::Paned &paned) { - paned.property_position().signal_changed().connect(sigc::mem_fun(*this, &ChannelList::OnPanedPositionChanged)); -} - -void ChannelList::OnPanedPositionChanged() { - m_view.queue_draw(); -} - -void ChannelList::UpdateListing() { - m_updating_listing = true; - - m_model->clear(); - - auto &discord = Abaddon::Get().GetDiscordClient(); - - /* - guild_folders looks something like this - "guild_folders": [ - { - "color": null, - "guild_ids": [ - "8009060___________" - ], - "id": null, - "name": null - }, - { - "color": null, - "guild_ids": [ - "99615594__________", - "86132141__________", - "35450138__________", - "83714048__________" - ], - "id": 2853066769, - "name": null - } - ] - - so if id != null then its a folder (they can have single entries) - */ - - int sort_value = 0; - - const auto folders = discord.GetUserSettings().GuildFolders; - const auto guild_ids = discord.GetUserSortedGuilds(); - - // user_settings.guild_folders may not contain every guild the user is in - // this seems to be the case if you organize your guilds and join a server without further organization - // so add guilds not present in guild_folders by descending id order first - - std::set foldered_guilds; - for (const auto &group : folders) { - foldered_guilds.insert(group.GuildIDs.begin(), group.GuildIDs.end()); - } - - for (auto iter = guild_ids.rbegin(); iter != guild_ids.rend(); iter++) { - if (foldered_guilds.find(*iter) == foldered_guilds.end()) { - const auto guild = discord.GetGuild(*iter); - if (!guild.has_value()) continue; - auto tree_iter = AddGuild(*guild, m_model->children()); - if (tree_iter) (*tree_iter)[m_columns.m_sort] = sort_value++; - } - } - - // then whatever is in folders - - for (const auto &group : folders) { - auto iter = AddFolder(group); - if (iter) (*iter)[m_columns.m_sort] = sort_value++; - } - - m_updating_listing = false; - - AddPrivateChannels(); -} - -// TODO update for folders -void ChannelList::UpdateNewGuild(const GuildData &guild) { - AddGuild(guild, m_model->children()); - // 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 = GetIteratorForRowFromID(id); - if (!iter) return; - m_model->erase(iter); -} - -void ChannelList::UpdateChannel(Snowflake id) { - auto iter = GetIteratorForRowFromID(id); - auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id); - if (!iter || !channel.has_value()) return; - if (channel->Type == ChannelType::GUILD_CATEGORY) return UpdateChannelCategory(*channel); - if (!channel->IsText()) 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 = GetIteratorForRowFromID(*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 = GetIteratorForRowFromID(*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; - - (*iter)[m_columns.m_name] = "" + Glib::Markup::escape_text(guild->Name) + ""; - (*iter)[m_columns.m_icon] = img.GetPlaceholder(GuildIconSize); - if (Abaddon::Get().GetSettings().ShowAnimations && guild->HasAnimatedIcon()) { - const auto cb = [this, id](const Glib::RefPtr &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 &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 (GetIteratorForRowFromID(id)) return; - const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id); - if (!channel.has_value()) return; - const auto parent = GetIteratorForRowFromID(*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 = GetIteratorForRowFromID(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 threads; - auto guild_iter = GetIteratorForGuildFromID(data.GuildID); - if (!guild_iter) return; - - std::queue 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((*item)[m_columns.m_id])); - for (const 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 = GetIteratorForRowFromID(thread_id); - m_model->erase(iter); - } - } - - // delete all archived threads - for (auto thread : data.Threads) { - if (thread.ThreadMetadata->IsArchived) { - if (auto iter = GetIteratorForRowFromID(thread.ID)) - m_model->erase(iter); - } - } -} - -#ifdef WITH_VOICE -void ChannelList::OnVoiceUserConnect(Snowflake user_id, Snowflake channel_id) { - auto parent_iter = GetIteratorForRowFromIDOfType(channel_id, RenderType::VoiceChannel); - if (!parent_iter) parent_iter = GetIteratorForRowFromIDOfType(channel_id, RenderType::DM); - if (!parent_iter) return; - const auto user = Abaddon::Get().GetDiscordClient().GetUser(user_id); - if (!user.has_value()) return; - - CreateVoiceParticipantRow(*user, parent_iter->children()); -} - -void ChannelList::OnVoiceUserDisconnect(Snowflake user_id, Snowflake channel_id) { - if (auto iter = GetIteratorForRowFromIDOfType(user_id, RenderType::VoiceParticipant)) { - m_model->erase(iter); - } -} - -void ChannelList::OnVoiceStateSet(Snowflake user_id, Snowflake channel_id, VoiceStateFlags flags) { - if (auto iter = GetIteratorForRowFromIDOfType(user_id, RenderType::VoiceParticipant)) { - (*iter)[m_columns.m_voice_flags] = flags; - } -} -#endif - -void ChannelList::DeleteThreadRow(Snowflake id) { - auto iter = GetIteratorForRowFromID(id); - if (iter) - m_model->erase(iter); -} - -void ChannelList::OnChannelMute(Snowflake id) { - if (auto iter = GetIteratorForRowFromID(id)) - m_model->row_changed(m_model->get_path(iter), iter); -} - -void ChannelList::OnChannelUnmute(Snowflake id) { - if (auto iter = GetIteratorForRowFromID(id)) - m_model->row_changed(m_model->get_path(iter), iter); -} - -void ChannelList::OnGuildMute(Snowflake id) { - if (auto iter = GetIteratorForGuildFromID(id)) - m_model->row_changed(m_model->get_path(iter), iter); -} - -void ChannelList::OnGuildUnmute(Snowflake id) { - if (auto iter = GetIteratorForGuildFromID(id)) - m_model->row_changed(m_model->get_path(iter), 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, bool expand_to) { - // mark channel as read when switching off - if (m_active_channel.IsValid()) - Abaddon::Get().GetDiscordClient().MarkChannelAsRead(m_active_channel, [](...) {}); - - m_active_channel = id; - - if (m_temporary_thread_row) { - const auto thread_id = static_cast((*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 = GetIteratorForRowFromID(id); - if (channel_iter) { - if (expand_to) { - 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(); - const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id); - if (!channel.has_value() || !channel->IsThread()) return; - auto parent_iter = GetIteratorForRowFromID(*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 { - for (const auto &[id, state] : root.Children) { - Gtk::TreeModel::iterator row_iter; - if (const auto map_iter = m_tmp_row_map.find(id); map_iter != m_tmp_row_map.end()) { - row_iter = map_iter->second; - } else if (const auto map_iter = m_tmp_guild_row_map.find(id); map_iter != m_tmp_guild_row_map.end()) { - row_iter = map_iter->second; - } - - if (row_iter) { - if (state.IsExpanded) - m_view.expand_row(m_model->get_path(row_iter), false); - else - m_view.collapse_row(m_model->get_path(row_iter)); - } - - self(self, state.Children); - } - }; - - for (const auto &[id, state] : root.Children) { - if (const auto iter = GetIteratorForTopLevelFromID(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); - } - - m_tmp_row_map.clear(); -} - -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(child[m_columns.m_id])] = self(self, child); - - return r; - }; - - for (const auto &child : m_model->children()) { - const auto id = static_cast(child[m_columns.m_id]); - if (static_cast(id) == 0ULL) continue; // dont save DM header - r.Children[id] = recurse(recurse, child); - } - - return r; -} - -Gtk::TreeModel::iterator ChannelList::AddFolder(const UserSettingsGuildFoldersEntry &folder) { - if (!folder.ID.has_value()) { - // just a guild - if (!folder.GuildIDs.empty()) { - const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(folder.GuildIDs[0]); - if (guild.has_value()) { - return AddGuild(*guild, m_model->children()); - } - } - } else { - auto folder_row = *m_model->append(); - folder_row[m_columns.m_type] = RenderType::Folder; - folder_row[m_columns.m_id] = *folder.ID; - m_tmp_row_map[*folder.ID] = folder_row; - if (folder.Name.has_value()) { - folder_row[m_columns.m_name] = Glib::Markup::escape_text(*folder.Name); - } else { - folder_row[m_columns.m_name] = "Folder"; - } - if (folder.Color.has_value()) { - folder_row[m_columns.m_color] = IntToRGBA(*folder.Color); - } - - int sort_value = 0; - for (const auto &guild_id : folder.GuildIDs) { - const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(guild_id); - if (guild.has_value()) { - auto guild_row = AddGuild(*guild, folder_row->children()); - (*guild_row)[m_columns.m_sort] = sort_value++; - } - } - - return folder_row; - } - - return {}; -} - -Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild, const Gtk::TreeNodeChildren &root) { - auto &discord = Abaddon::Get().GetDiscordClient(); - auto &img = Abaddon::Get().GetImageManager(); - - auto guild_row = *m_model->append(root); - guild_row[m_columns.m_type] = RenderType::Guild; - guild_row[m_columns.m_id] = guild.ID; - guild_row[m_columns.m_name] = "" + Glib::Markup::escape_text(guild.Name) + ""; - guild_row[m_columns.m_icon] = img.GetPlaceholder(GuildIconSize); - m_tmp_guild_row_map[guild.ID] = guild_row; - - if (Abaddon::Get().GetSettings().ShowAnimations && guild.HasAnimatedIcon()) { - const auto cb = [this, id = guild.ID](const Glib::RefPtr &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 &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 orphan_channels; - std::map> categories; - - for (const auto &channel_ : *guild.Channels) { - const auto channel = discord.GetChannel(channel_.ID); - if (!channel.has_value()) continue; -#ifdef WITH_VOICE - if (channel->Type == ChannelType::GUILD_TEXT || channel->Type == ChannelType::GUILD_NEWS || channel->Type == ChannelType::GUILD_VOICE) { -#else - if (channel->Type == ChannelType::GUILD_TEXT || channel->Type == ChannelType::GUILD_NEWS) { -#endif - 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> 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, const 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) - m_tmp_row_map[thread.ID] = CreateThreadRow(row.children(), thread); - }; - -#ifdef WITH_VOICE - auto add_voice_participants = [this, &discord](const ChannelData &channel, const Gtk::TreeNodeChildren &root) { - for (auto user_id : discord.GetUsersInVoiceChannel(channel.ID)) { - if (const auto user = discord.GetUser(user_id); user.has_value()) { - CreateVoiceParticipantRow(*user, root); - } - } - }; -#endif - - for (const auto &channel : orphan_channels) { - auto channel_row = *m_model->append(guild_row.children()); - if (IsTextChannel(channel.Type)) { - channel_row[m_columns.m_type] = RenderType::TextChannel; - channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name); - } -#ifdef WITH_VOICE - else { - channel_row[m_columns.m_type] = RenderType::VoiceChannel; - channel_row[m_columns.m_name] = Glib::Markup::escape_text(*channel.Name); - add_voice_participants(channel, channel_row->children()); - } -#endif - channel_row[m_columns.m_id] = channel.ID; - channel_row[m_columns.m_sort] = *channel.Position + OrphanChannelSortOffset; - channel_row[m_columns.m_nsfw] = channel.NSFW(); - add_threads(channel, channel_row); - m_tmp_row_map[channel.ID] = 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_tmp_row_map[category_id] = cat_row; - // 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()); - if (IsTextChannel(channel.Type)) { - channel_row[m_columns.m_type] = RenderType::TextChannel; - channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name); - } -#ifdef WITH_VOICE - else { - channel_row[m_columns.m_type] = RenderType::VoiceChannel; - channel_row[m_columns.m_name] = Glib::Markup::escape_text(*channel.Name); - add_voice_participants(channel, channel_row->children()); - } -#endif - channel_row[m_columns.m_id] = channel.ID; - channel_row[m_columns.m_sort] = *channel.Position; - channel_row[m_columns.m_nsfw] = channel.NSFW(); - add_threads(channel, channel_row); - m_tmp_row_map[channel.ID] = 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] = static_cast(channel.ID); - thread_row[m_columns.m_nsfw] = false; - - return thread_iter; -} - -#ifdef WITH_VOICE -Gtk::TreeModel::iterator ChannelList::CreateVoiceParticipantRow(const UserData &user, const Gtk::TreeNodeChildren &parent) { - auto row = *m_model->append(parent); - row[m_columns.m_type] = RenderType::VoiceParticipant; - row[m_columns.m_id] = user.ID; - row[m_columns.m_name] = user.GetDisplayNameEscaped(); - - const auto voice_state = Abaddon::Get().GetDiscordClient().GetVoiceState(user.ID); - if (voice_state.has_value()) { - row[m_columns.m_voice_flags] = voice_state->second; - } - - auto &img = Abaddon::Get().GetImageManager(); - row[m_columns.m_icon] = img.GetPlaceholder(VoiceParticipantIconSize); - const auto cb = [this, user_id = user.ID](const Glib::RefPtr &pb) { - auto iter = GetIteratorForRowFromIDOfType(user_id, RenderType::VoiceParticipant); - if (iter) (*iter)[m_columns.m_icon] = pb->scale_simple(VoiceParticipantIconSize, VoiceParticipantIconSize, Gdk::INTERP_BILINEAR); - }; - img.LoadFromURL(user.GetAvatarURL("png", "32"), sigc::track_obj(cb, *this)); - - return row; -} -#endif - -void ChannelList::UpdateChannelCategory(const ChannelData &channel) { - auto iter = GetIteratorForRowFromID(channel.ID); - if (!iter) return; - - (*iter)[m_columns.m_sort] = *channel.Position; - (*iter)[m_columns.m_name] = Glib::Markup::escape_text(*channel.Name); -} - -// todo this all needs refactoring for shooore -Gtk::TreeModel::iterator ChannelList::GetIteratorForTopLevelFromID(Snowflake id) { - for (const auto &child : m_model->children()) { - if ((child[m_columns.m_type] == RenderType::Guild || child[m_columns.m_type] == RenderType::Folder) && child[m_columns.m_id] == id) { - return child; - } else if (child[m_columns.m_type] == RenderType::Folder) { - for (const auto &folder_child : child->children()) { - if (folder_child[m_columns.m_id] == id) { - return folder_child; - } - } - } - } - return {}; -} - -Gtk::TreeModel::iterator ChannelList::GetIteratorForGuildFromID(Snowflake id) { - for (const auto &child : m_model->children()) { - if (child[m_columns.m_type] == RenderType::Guild && child[m_columns.m_id] == id) { - return child; - } else if (child[m_columns.m_type] == RenderType::Folder) { - for (const auto &folder_child : child->children()) { - if (folder_child[m_columns.m_id] == id) { - return folder_child; - } - } - } - } - return {}; -} - -Gtk::TreeModel::iterator ChannelList::GetIteratorForRowFromID(Snowflake id) { - std::queue 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 && (*item)[m_columns.m_type] != RenderType::Guild) return item; - for (const auto &child : item->children()) - queue.push(child); - queue.pop(); - } - - return {}; -} - -Gtk::TreeModel::iterator ChannelList::GetIteratorForRowFromIDOfType(Snowflake id, RenderType type) { - std::queue 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_type] == type && (*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) const { - (*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 &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] = "Direct Messages"; - 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 top_recipient; - const auto recipients = dm->GetDMRecipients(); - if (!recipients.empty()) - 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_name] = Glib::Markup::escape_text(dm->GetDisplayName()); - row[m_columns.m_sort] = static_cast(-(dm->LastMessageID.has_value() ? *dm->LastMessageID : dm_id)); - row[m_columns.m_icon] = img.GetPlaceholder(DMIconSize); - row[m_columns.m_expanded] = true; - -#ifdef WITH_VOICE - for (auto user_id : discord.GetUsersInVoiceChannel(dm_id)) { - if (const auto user = discord.GetUser(user_id); user.has_value()) { - CreateVoiceParticipantRow(*user, row->children()); - } - } -#endif - - SetDMChannelIcon(iter, *dm); - } -} - -void ChannelList::UpdateCreateDMChannel(const ChannelData &dm) { - auto header_row = m_model->get_iter(m_dm_header); - auto &img = Abaddon::Get().GetImageManager(); - - 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_name] = Glib::Markup::escape_text(dm.GetDisplayName()); - row[m_columns.m_sort] = static_cast(-(dm.LastMessageID.has_value() ? *dm.LastMessageID : dm.ID)); - row[m_columns.m_icon] = img.GetPlaceholder(DMIconSize); - - SetDMChannelIcon(iter, dm); -} - -void ChannelList::SetDMChannelIcon(Gtk::TreeIter iter, const ChannelData &dm) { - auto &img = Abaddon::Get().GetImageManager(); - - std::optional top_recipient; - const auto recipients = dm.GetDMRecipients(); - if (!recipients.empty()) - top_recipient = recipients[0]; - - if (dm.HasIcon()) { - const auto cb = [this, iter](const Glib::RefPtr &pb) { - if (iter) - (*iter)[m_columns.m_icon] = pb->scale_simple(DMIconSize, DMIconSize, Gdk::INTERP_BILINEAR); - }; - img.LoadFromURL(dm.GetIconURL(), sigc::track_obj(cb, *this)); - } else if (dm.Type == ChannelType::DM && top_recipient.has_value()) { - const auto cb = [this, iter](const Glib::RefPtr &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)); - } else { // GROUP_DM - std::string hash; - switch (dm.ID.GetUnixMilliseconds() % 8) { - case 0: - hash = "ee9275c5a437f7dc7f9430ba95f12ebd"; - break; - case 1: - hash = "9baf45aac2a0ec2e2dab288333acb9d9"; - break; - case 2: - hash = "7ba11ffb1900fa2b088cb31324242047"; - break; - case 3: - hash = "f90fca70610c4898bc57b58bce92f587"; - break; - case 4: - hash = "e2779af34b8d9126b77420e5f09213ce"; - break; - case 5: - hash = "c6851bd0b03f1cca5a8c1e720ea6ea17"; - break; - case 6: - hash = "f7e38ac976a2a696161c923502a8345b"; - break; - case 7: - default: - hash = "3cb840d03313467838d658bbec801fcd"; - break; - } - const auto cb = [this, iter](const Glib::RefPtr &pb) { - if (iter) - (*iter)[m_columns.m_icon] = pb->scale_simple(DMIconSize, DMIconSize, Gdk::INTERP_BILINEAR); - }; - img.LoadFromURL("https://discord.com/assets/" + hash + ".png", sigc::track_obj(cb, *this)); - } -} - -void ChannelList::RedrawUnreadIndicatorsForChannel(const ChannelData &channel) { - if (channel.GuildID.has_value()) { - auto iter = GetIteratorForGuildFromID(*channel.GuildID); - if (iter) m_model->row_changed(m_model->get_path(iter), iter); - } - if (channel.ParentID.has_value()) { - auto iter = GetIteratorForRowFromIDOfType(*channel.ParentID, RenderType::Category); - if (iter) m_model->row_changed(m_model->get_path(iter), iter); - } -} - -void ChannelList::OnMessageAck(const MessageAckData &data) { - // trick renderer into redrawing - m_model->row_changed(Gtk::TreeModel::Path("0"), m_model->get_iter("0")); // 0 is always path for dm header - auto iter = GetIteratorForRowFromID(data.ChannelID); - if (iter) m_model->row_changed(m_model->get_path(iter), iter); - auto channel = Abaddon::Get().GetDiscordClient().GetChannel(data.ChannelID); - if (channel.has_value()) { - RedrawUnreadIndicatorsForChannel(*channel); - } -} - -void ChannelList::OnMessageCreate(const Message &msg) { - auto iter = GetIteratorForRowFromID(msg.ChannelID); - if (iter) m_model->row_changed(m_model->get_path(iter), iter); // redraw - const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(msg.ChannelID); - if (!channel.has_value()) return; - if (channel->Type == ChannelType::DM || channel->Type == ChannelType::GROUP_DM) { - if (iter) - (*iter)[m_columns.m_sort] = static_cast(-msg.ID); - } - RedrawUnreadIndicatorsForChannel(*channel); -} - -bool ChannelList::OnButtonPressEvent(GdkEventButton *ev) { - if (ev->button == GDK_BUTTON_SECONDARY && ev->type == GDK_BUTTON_PRESS) { - if (m_view.get_path_at_pos(static_cast(ev->x), static_cast(ev->y), m_path_for_menu)) { - auto row = (*m_model->get_iter(m_path_for_menu)); - switch (static_cast(row[m_columns.m_type])) { - case RenderType::Guild: - OnGuildSubmenuPopup(); - m_menu_guild.popup_at_pointer(reinterpret_cast(ev)); - break; - case RenderType::Category: - OnCategorySubmenuPopup(); - m_menu_category.popup_at_pointer(reinterpret_cast(ev)); - break; - case RenderType::TextChannel: - OnChannelSubmenuPopup(); - m_menu_channel.popup_at_pointer(reinterpret_cast(ev)); - break; -#ifdef WITH_VOICE - case RenderType::VoiceChannel: - OnVoiceChannelSubmenuPopup(); - m_menu_voice_channel.popup_at_pointer(reinterpret_cast(ev)); - break; -#endif - case RenderType::DM: { - OnDMSubmenuPopup(); - const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(static_cast(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(ev)); - } break; - case RenderType::Thread: { - OnThreadSubmenuPopup(); - m_menu_thread.popup_at_pointer(reinterpret_cast(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((*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); - M(m_color); -#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(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::OnGuildSubmenuPopup() { - const auto iter = m_model->get_iter(m_path_for_menu); - if (!iter) return; - const auto id = static_cast((*iter)[m_columns.m_id]); - auto &discord = Abaddon::Get().GetDiscordClient(); - if (discord.IsGuildMuted(id)) - m_menu_guild_toggle_mute.set_label("Unmute"); - else - m_menu_guild_toggle_mute.set_label("Mute"); - - const auto guild = discord.GetGuild(id); - const auto self_id = discord.GetUserData().ID; - m_menu_guild_leave.set_sensitive(!(guild.has_value() && guild->OwnerID == self_id)); -} - -void ChannelList::OnCategorySubmenuPopup() { - const auto iter = m_model->get_iter(m_path_for_menu); - if (!iter) return; - const auto id = static_cast((*iter)[m_columns.m_id]); - if (Abaddon::Get().GetDiscordClient().IsChannelMuted(id)) - m_menu_category_toggle_mute.set_label("Unmute"); - else - m_menu_category_toggle_mute.set_label("Mute"); -} - -void ChannelList::OnChannelSubmenuPopup() { - const auto iter = m_model->get_iter(m_path_for_menu); - if (!iter) return; - const auto id = static_cast((*iter)[m_columns.m_id]); - auto &discord = Abaddon::Get().GetDiscordClient(); -#ifdef WITH_LIBHANDY - const auto perms = discord.HasChannelPermission(discord.GetUserData().ID, id, Permission::VIEW_CHANNEL); - m_menu_channel_open_tab.set_sensitive(perms); -#endif - if (discord.IsChannelMuted(id)) - m_menu_channel_toggle_mute.set_label("Unmute"); - else - m_menu_channel_toggle_mute.set_label("Mute"); -} - -#ifdef WITH_VOICE -void ChannelList::OnVoiceChannelSubmenuPopup() { - const auto iter = m_model->get_iter(m_path_for_menu); - if (!iter) return; - const auto id = static_cast((*iter)[m_columns.m_id]); - auto &discord = Abaddon::Get().GetDiscordClient(); - if (discord.IsVoiceConnected() || discord.IsVoiceConnecting()) { - m_menu_voice_channel_join.set_sensitive(false); - m_menu_voice_channel_disconnect.set_sensitive(discord.GetVoiceChannelID() == id); - } else { - m_menu_voice_channel_join.set_sensitive(true); - m_menu_voice_channel_disconnect.set_sensitive(false); - } -} -#endif - -void ChannelList::OnDMSubmenuPopup() { - auto iter = m_model->get_iter(m_path_for_menu); - if (!iter) return; - const auto id = static_cast((*iter)[m_columns.m_id]); - auto &discord = Abaddon::Get().GetDiscordClient(); - if (discord.IsChannelMuted(id)) - m_menu_dm_toggle_mute.set_label("Unmute"); - else - m_menu_dm_toggle_mute.set_label("Mute"); - -#ifdef WITH_VOICE - if (discord.IsVoiceConnected() || discord.IsVoiceConnecting()) { - m_menu_dm_join_voice.set_sensitive(false); - m_menu_dm_disconnect_voice.set_sensitive(discord.GetVoiceChannelID() == id); - } else { - m_menu_dm_join_voice.set_sensitive(true); - m_menu_dm_disconnect_voice.set_sensitive(false); - } -#endif -} - -void ChannelList::OnThreadSubmenuPopup() { - 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; - const auto id = static_cast((*iter)[m_columns.m_id]); - - if (discord.IsChannelMuted(id)) - m_menu_thread_toggle_mute.set_label("Unmute"); - else - m_menu_thread_toggle_mute.set_label("Mute"); - - auto channel = discord.GetChannel(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; -} - -#ifdef WITH_LIBHANDY -ChannelList::type_signal_action_open_new_tab ChannelList::signal_action_open_new_tab() { - return m_signal_action_open_new_tab; -} -#endif - -#ifdef WITH_VOICE -ChannelList::type_signal_action_join_voice_channel ChannelList::signal_action_join_voice_channel() { - return m_signal_action_join_voice_channel; -} - -ChannelList::type_signal_action_disconnect_voice ChannelList::signal_action_disconnect_voice() { - return m_signal_action_disconnect_voice; -} -#endif - -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); - add(m_color); - add(m_voice_flags); -} diff --git a/src/components/channels.hpp b/src/components/channels.hpp deleted file mode 100644 index 9d449e4..0000000 --- a/src/components/channels.hpp +++ /dev/null @@ -1,229 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "discord/discord.hpp" -#include "state.hpp" -#include "channelscellrenderer.hpp" - -constexpr static int GuildIconSize = 24; -constexpr static int DMIconSize = 20; -constexpr static int VoiceParticipantIconSize = 18; -constexpr static int OrphanChannelSortOffset = -100; // forces orphan channels to the top of the list - -class ChannelList : public Gtk::ScrolledWindow { -public: - ChannelList(); - - void UpdateListing(); - void SetActiveChannel(Snowflake id, bool expand_to); - - // channel list should be populated when this is called - void UseExpansionState(const ExpansionStateRoot &state); - ExpansionStateRoot GetExpansionState() const; - - void UsePanedHack(Gtk::Paned &paned); - -protected: - void OnPanedPositionChanged(); - - void UpdateNewGuild(const GuildData &guild); - void UpdateRemoveGuild(Snowflake id); - void UpdateRemoveChannel(Snowflake id); - void UpdateChannel(Snowflake id); - void UpdateCreateChannel(const ChannelData &channel); - void UpdateGuild(Snowflake id); - void DeleteThreadRow(Snowflake id); - void OnChannelMute(Snowflake id); - void OnChannelUnmute(Snowflake id); - void OnGuildMute(Snowflake id); - void OnGuildUnmute(Snowflake id); - - void OnThreadJoined(Snowflake id); - void OnThreadRemoved(Snowflake id); - void OnThreadDelete(const ThreadDeleteData &data); - void OnThreadUpdate(const ThreadUpdateData &data); - void OnThreadListSync(const ThreadListSyncData &data); - - void OnVoiceUserConnect(Snowflake user_id, Snowflake channel_id); - void OnVoiceUserDisconnect(Snowflake user_id, Snowflake channel_id); - void OnVoiceStateSet(Snowflake user_id, Snowflake channel_id, VoiceStateFlags flags); - - Gtk::TreeView m_view; - - class ModelColumns : public Gtk::TreeModel::ColumnRecord { - public: - ModelColumns(); - - Gtk::TreeModelColumn m_type; - Gtk::TreeModelColumn m_id; - Gtk::TreeModelColumn m_name; - Gtk::TreeModelColumn> m_icon; - Gtk::TreeModelColumn> m_icon_anim; - Gtk::TreeModelColumn m_sort; - Gtk::TreeModelColumn m_nsfw; - Gtk::TreeModelColumn> m_color; // for folders right now - Gtk::TreeModelColumn m_voice_flags; - // Gtk::CellRenderer's property_is_expanded only works how i want it to if it has children - // because otherwise it doesnt count as an "expander" (property_is_expander) - // so this solution will have to do which i hate but the alternative is adding invisible children - // to all categories without children and having a filter model but that sounds worse - // of course its a lot better than the absolute travesty i had before - Gtk::TreeModelColumn m_expanded; - }; - - ModelColumns m_columns; - Glib::RefPtr m_model; - - Gtk::TreeModel::iterator AddFolder(const UserSettingsGuildFoldersEntry &folder); - Gtk::TreeModel::iterator AddGuild(const GuildData &guild, const Gtk::TreeNodeChildren &root); - Gtk::TreeModel::iterator UpdateCreateChannelCategory(const ChannelData &channel); - Gtk::TreeModel::iterator CreateThreadRow(const Gtk::TreeNodeChildren &children, const ChannelData &channel); - -#ifdef WITH_VOICE - Gtk::TreeModel::iterator CreateVoiceParticipantRow(const UserData &user, const Gtk::TreeNodeChildren &parent); -#endif - - void UpdateChannelCategory(const ChannelData &channel); - - // separation necessary because a channel and guild can share the same id - Gtk::TreeModel::iterator GetIteratorForTopLevelFromID(Snowflake id); - Gtk::TreeModel::iterator GetIteratorForGuildFromID(Snowflake id); - Gtk::TreeModel::iterator GetIteratorForRowFromID(Snowflake id); - Gtk::TreeModel::iterator GetIteratorForRowFromIDOfType(Snowflake id, RenderType type); - - bool IsTextChannel(ChannelType type); - - void OnRowCollapsed(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path) const; - void OnRowExpanded(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path); - bool SelectionFunc(const Glib::RefPtr &model, const Gtk::TreeModel::Path &path, bool is_currently_selected); - bool OnButtonPressEvent(GdkEventButton *ev); - - void MoveRow(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::iterator &new_parent); - - Gtk::TreeModel::Path m_last_selected; - Gtk::TreeModel::Path m_dm_header; - - void AddPrivateChannels(); - void UpdateCreateDMChannel(const ChannelData &channel); - void SetDMChannelIcon(Gtk::TreeIter iter, const ChannelData &dm); - - void RedrawUnreadIndicatorsForChannel(const ChannelData& channel); - void OnMessageAck(const MessageAckData &data); - void OnMessageCreate(const Message &msg); - - Gtk::TreeModel::Path m_path_for_menu; - - // cant be recovered through selection - Gtk::TreeModel::iterator m_temporary_thread_row; - - Gtk::Menu m_menu_guild; - Gtk::MenuItem m_menu_guild_copy_id; - Gtk::MenuItem m_menu_guild_settings; - Gtk::MenuItem m_menu_guild_leave; - Gtk::MenuItem m_menu_guild_mark_as_read; - Gtk::MenuItem m_menu_guild_toggle_mute; - - Gtk::Menu m_menu_category; - Gtk::MenuItem m_menu_category_copy_id; - Gtk::MenuItem m_menu_category_toggle_mute; - - Gtk::Menu m_menu_channel; - Gtk::MenuItem m_menu_channel_copy_id; - Gtk::MenuItem m_menu_channel_mark_as_read; - Gtk::MenuItem m_menu_channel_toggle_mute; - -#ifdef WITH_LIBHANDY - Gtk::MenuItem m_menu_channel_open_tab; -#endif - -#ifdef WITH_VOICE - Gtk::Menu m_menu_voice_channel; - Gtk::MenuItem m_menu_voice_channel_join; - Gtk::MenuItem m_menu_voice_channel_disconnect; -#endif - - Gtk::Menu m_menu_dm; - Gtk::MenuItem m_menu_dm_copy_id; - Gtk::MenuItem m_menu_dm_close; - Gtk::MenuItem m_menu_dm_toggle_mute; -#ifdef WITH_VOICE - Gtk::MenuItem m_menu_dm_join_voice; - Gtk::MenuItem m_menu_dm_disconnect_voice; -#endif - -#ifdef WITH_LIBHANDY - Gtk::MenuItem m_menu_dm_open_tab; -#endif - - Gtk::Menu m_menu_thread; - Gtk::MenuItem m_menu_thread_copy_id; - Gtk::MenuItem m_menu_thread_leave; - Gtk::MenuItem m_menu_thread_archive; - Gtk::MenuItem m_menu_thread_unarchive; - Gtk::MenuItem m_menu_thread_mark_as_read; - Gtk::MenuItem m_menu_thread_toggle_mute; - - void OnGuildSubmenuPopup(); - void OnCategorySubmenuPopup(); - void OnChannelSubmenuPopup(); - void OnDMSubmenuPopup(); - void OnThreadSubmenuPopup(); - -#ifdef WITH_VOICE - void OnVoiceChannelSubmenuPopup(); -#endif - - bool m_updating_listing = false; - - Snowflake m_active_channel; - - // (GetIteratorForChannelFromID is rather slow) - // only temporary since i dont want to worry about maintaining this map - std::unordered_map m_tmp_row_map; - std::unordered_map m_tmp_guild_row_map; - -public: - using type_signal_action_channel_item_select = sigc::signal; - using type_signal_action_guild_leave = sigc::signal; - using type_signal_action_guild_settings = sigc::signal; - -#ifdef WITH_LIBHANDY - using type_signal_action_open_new_tab = sigc::signal; - type_signal_action_open_new_tab signal_action_open_new_tab(); -#endif - -#ifdef WITH_VOICE - using type_signal_action_join_voice_channel = sigc::signal; - using type_signal_action_disconnect_voice = sigc::signal; - - type_signal_action_join_voice_channel signal_action_join_voice_channel(); - type_signal_action_disconnect_voice signal_action_disconnect_voice(); -#endif - - type_signal_action_channel_item_select signal_action_channel_item_select(); - type_signal_action_guild_leave signal_action_guild_leave(); - type_signal_action_guild_settings signal_action_guild_settings(); - -private: - type_signal_action_channel_item_select m_signal_action_channel_item_select; - type_signal_action_guild_leave m_signal_action_guild_leave; - type_signal_action_guild_settings m_signal_action_guild_settings; - -#ifdef WITH_LIBHANDY - type_signal_action_open_new_tab m_signal_action_open_new_tab; -#endif - -#ifdef WITH_VOICE - type_signal_action_join_voice_channel m_signal_action_join_voice_channel; - type_signal_action_disconnect_voice m_signal_action_disconnect_voice; -#endif -}; diff --git a/src/components/channelscellrenderer.cpp b/src/components/channelscellrenderer.cpp deleted file mode 100644 index ac98512..0000000 --- a/src/components/channelscellrenderer.cpp +++ /dev/null @@ -1,970 +0,0 @@ -#include "channelscellrenderer.hpp" - -constexpr static int MentionsRightPad = 7; -#ifndef M_PI -constexpr static double M_PI = 3.14159265358979; -#endif -constexpr static double M_PI_H = M_PI / 2.0; -constexpr static double M_PI_3_2 = M_PI * 3.0 / 2.0; - -CellRendererChannels::CellRendererChannels() - : Glib::ObjectBase(typeid(CellRendererChannels)) - , Gtk::CellRenderer() - , m_property_type(*this, "render-type") - , m_property_id(*this, "id") - , 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") - , m_property_color(*this, "color") - , m_property_voice_state(*this, "voice-state") { - 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; - }); -} - -Glib::PropertyProxy CellRendererChannels::property_type() { - return m_property_type.get_proxy(); -} - -Glib::PropertyProxy CellRendererChannels::property_id() { - return m_property_id.get_proxy(); -} - -Glib::PropertyProxy CellRendererChannels::property_name() { - return m_property_name.get_proxy(); -} - -Glib::PropertyProxy> CellRendererChannels::property_icon() { - return m_property_pixbuf.get_proxy(); -} - -Glib::PropertyProxy> CellRendererChannels::property_icon_animation() { - return m_property_pixbuf_animation.get_proxy(); -} - -Glib::PropertyProxy CellRendererChannels::property_expanded() { - return m_property_expanded.get_proxy(); -} - -Glib::PropertyProxy CellRendererChannels::property_nsfw() { - return m_property_nsfw.get_proxy(); -} - -Glib::PropertyProxy> CellRendererChannels::property_color() { - return m_property_color.get_proxy(); -} - -Glib::PropertyProxy CellRendererChannels::property_voice_state() { - return m_property_voice_state.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::Folder: - return get_preferred_width_vfunc_folder(widget, minimum_width, natural_width); - 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); -#ifdef WITH_VOICE - case RenderType::VoiceChannel: - return get_preferred_width_vfunc_voice_channel(widget, minimum_width, natural_width); - case RenderType::VoiceParticipant: - return get_preferred_width_vfunc_voice_participant(widget, minimum_width, natural_width); -#endif - 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::Folder: - return get_preferred_width_for_height_vfunc_folder(widget, height, minimum_width, natural_width); - 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); -#ifdef WITH_VOICE - case RenderType::VoiceChannel: - return get_preferred_width_for_height_vfunc_voice_channel(widget, height, minimum_width, natural_width); - case RenderType::VoiceParticipant: - return get_preferred_width_for_height_vfunc_voice_participant(widget, height, minimum_width, natural_width); -#endif - 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::Folder: - return get_preferred_height_vfunc_folder(widget, minimum_height, natural_height); - 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); -#ifdef WITH_VOICE - case RenderType::VoiceChannel: - return get_preferred_height_vfunc_voice_channel(widget, minimum_height, natural_height); - case RenderType::VoiceParticipant: - return get_preferred_height_vfunc_voice_participant(widget, minimum_height, natural_height); -#endif - 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::Folder: - return get_preferred_height_for_width_vfunc_folder(widget, width, minimum_height, natural_height); - 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); -#ifdef WITH_VOICE - case RenderType::VoiceChannel: - return get_preferred_height_for_width_vfunc_voice_channel(widget, width, minimum_height, natural_height); - case RenderType::VoiceParticipant: - return get_preferred_height_for_width_vfunc_voice_participant(widget, width, minimum_height, natural_height); -#endif - 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 &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::Folder: - return render_vfunc_folder(cr, widget, background_area, cell_area, flags); - 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); -#ifdef WITH_VOICE - case RenderType::VoiceChannel: - return render_vfunc_voice_channel(cr, widget, background_area, cell_area, flags); - case RenderType::VoiceParticipant: - return render_vfunc_voice_participant(cr, widget, background_area, cell_area, flags); -#endif - 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); - } -} - -// folder functions - -void CellRendererChannels::get_preferred_width_vfunc_folder(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_folder(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_folder(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_folder(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_folder(const Cairo::RefPtr &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) { - 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); - const auto expander_color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelsExpanderColor); - 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); - - static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelColor); - if (m_property_color.get_value().has_value()) { - m_renderer_text.property_foreground_rgba() = *m_property_color.get_value(); - } else { - m_renderer_text.property_foreground_rgba() = color; - } - m_renderer_text.render(cr, widget, background_area, text_cell_area, flags); - m_renderer_text.property_foreground_set() = false; -} - -// 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 &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() + 3; - 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(static_cast(text_x), - static_cast(text_y), - static_cast(text_w), - static_cast(text_h)); - - static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelColor); - m_renderer_text.property_foreground_rgba() = color; - m_renderer_text.render(cr, widget, background_area, text_cell_area, flags); - - const bool hover_only = Abaddon::Get().GetSettings().AnimatedGuildHoverOnly; - 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( - static_cast(icon_x), - static_cast(icon_y), - static_cast(icon_w), - static_cast(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(); - } - - // unread - if (!Abaddon::Get().GetSettings().Unreads) return; - - const auto id = m_property_id.get_value(); - - auto &discord = Abaddon::Get().GetDiscordClient(); - int total_mentions; - const auto has_unread = discord.GetUnreadStateForGuild(id, total_mentions); - - if (has_unread && !discord.IsGuildMuted(id)) { - static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().UnreadIndicatorColor); - cr->set_source_rgb(color.get_red(), color.get_green(), color.get_blue()); - const auto x = background_area.get_x(); - const auto y = background_area.get_y(); - const auto w = background_area.get_width(); - const auto h = background_area.get_height(); - cr->rectangle(x, y + h / 2.0 - 24.0 / 2.0, 3.0, 24.0); - cr->fill(); - } - - if (total_mentions < 1) return; - auto *paned = dynamic_cast(widget.get_ancestor(Gtk::Paned::get_type())); - if (paned != nullptr) { - const auto edge = std::min(paned->get_position(), background_area.get_width()); - - unread_render_mentions(cr, widget, total_mentions, edge, background_area); - } -} - -// 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 AddUnreadIndicator(const Cairo::RefPtr &cr, const Gdk::Rectangle &background_area) { - static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().UnreadIndicatorColor); - cr->set_source_rgb(color.get_red(), color.get_green(), color.get_blue()); - const auto x = background_area.get_x(); - const auto y = background_area.get_y(); - const auto w = background_area.get_width(); - const auto h = background_area.get_height(); - cr->rectangle(x, y, 3, h); - cr->fill(); -} - -void CellRendererChannels::render_vfunc_category(const Cairo::RefPtr &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); - const auto expander_color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelsExpanderColor); - 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); - - static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelColor); - auto &discord = Abaddon::Get().GetDiscordClient(); - const auto id = m_property_id.get_value(); - if (discord.IsChannelMuted(m_property_id.get_value())) { - auto muted = color; - muted.set_red(muted.get_red() * 0.5); - muted.set_green(muted.get_green() * 0.5); - muted.set_blue(muted.get_blue() * 0.5); - m_renderer_text.property_foreground_rgba() = muted; - } else { - if (discord.GetUnreadChannelsCountForCategory(id) > 0) { - AddUnreadIndicator(cr, background_area); - } - m_renderer_text.property_foreground_rgba() = color; - } - m_renderer_text.render(cr, widget, background_area, text_cell_area, flags); - m_renderer_text.property_foreground_set() = false; -} - -// 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 &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); - - auto &discord = Abaddon::Get().GetDiscordClient(); - const auto id = m_property_id.get_value(); - const bool is_muted = discord.IsChannelMuted(id); - - static const auto sfw_unmuted = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelColor); - - m_renderer_text.property_sensitive() = false; - static const auto nsfw_color = Gdk::RGBA(Abaddon::Get().GetSettings().NSFWChannelColor); - if (m_property_nsfw.get_value()) - m_renderer_text.property_foreground_rgba() = nsfw_color; - else - m_renderer_text.property_foreground_rgba() = sfw_unmuted; - if (is_muted) { - auto col = m_renderer_text.property_foreground_rgba().get_value(); - col.set_red(col.get_red() * 0.5); - col.set_green(col.get_green() * 0.5); - col.set_blue(col.get_blue() * 0.5); - m_renderer_text.property_foreground_rgba() = col; - } - m_renderer_text.render(cr, widget, background_area, text_cell_area, flags); - // unset foreground to default so properties dont bleed - m_renderer_text.property_foreground_set() = false; - - // unread - if (!Abaddon::Get().GetSettings().Unreads) return; - - const auto unread_state = discord.GetUnreadStateForChannel(id); - if (unread_state < 0) return; - - if (!is_muted) { - AddUnreadIndicator(cr, background_area); - } - - if (unread_state < 1) return; - auto *paned = dynamic_cast(widget.get_ancestor(Gtk::Paned::get_type())); - if (paned != nullptr) { - const auto edge = std::min(paned->get_position(), cell_area.get_width()); - - unread_render_mentions(cr, widget, unread_state, edge, cell_area); - } -} - -// 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 &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); - - auto &discord = Abaddon::Get().GetDiscordClient(); - const auto id = m_property_id.get_value(); - const bool is_muted = discord.IsChannelMuted(id); - - static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelColor); - if (Abaddon::Get().GetDiscordClient().IsChannelMuted(m_property_id.get_value())) { - auto muted = color; - muted.set_red(muted.get_red() * 0.5); - muted.set_green(muted.get_green() * 0.5); - muted.set_blue(muted.get_blue() * 0.5); - m_renderer_text.property_foreground_rgba() = muted; - } else { - m_renderer_text.property_foreground_rgba() = color; - } - m_renderer_text.render(cr, widget, background_area, text_cell_area, flags); - m_renderer_text.property_foreground_set() = false; - - // unread - if (!Abaddon::Get().GetSettings().Unreads) return; - - const auto unread_state = discord.GetUnreadStateForChannel(id); - if (unread_state < 0) return; - - if (!is_muted) { - static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().UnreadIndicatorColor); - cr->set_source_rgb(color.get_red(), color.get_green(), color.get_blue()); - const auto x = background_area.get_x(); - const auto y = background_area.get_y(); - const auto w = background_area.get_width(); - const auto h = background_area.get_height(); - cr->rectangle(x, y, 3, h); - cr->fill(); - } - - if (unread_state < 1) return; - auto *paned = dynamic_cast(widget.get_ancestor(Gtk::Paned::get_type())); - if (paned != nullptr) { - const auto edge = std::min(paned->get_position(), cell_area.get_width()); - - unread_render_mentions(cr, widget, unread_state, edge, cell_area); - } -} - -#ifdef WITH_VOICE - -// voice channel - -void CellRendererChannels::get_preferred_width_vfunc_voice_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_voice_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_voice_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_voice_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_voice_channel(const Cairo::RefPtr &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) { - // channel name text - 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() + 35; - 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); - - // speaker character - Pango::FontDescription font; - font.set_family("sans 14"); - - auto layout = widget.create_pango_layout("\U0001F50A"); - layout->set_font_description(font); - layout->set_alignment(Pango::ALIGN_LEFT); - cr->set_source_rgba(1.0, 1.0, 1.0, 1.0); - int width, height; - layout->get_pixel_size(width, height); - cr->move_to( - background_area.get_x() + 1, - cell_area.get_y() + cell_area.get_height() / 2.0 - height / 2.0); - layout->show_in_cairo_context(cr); - - // expander - constexpr static int len = 5; - constexpr static int offset = 24; - int x1, y1, x2, y2, x3, y3; - if (property_expanded()) { - x1 = background_area.get_x() + offset; - y1 = background_area.get_y() + background_area.get_height() / 2 - len; - x2 = background_area.get_x() + offset + len; - y2 = background_area.get_y() + background_area.get_height() / 2 + len; - x3 = background_area.get_x() + offset + len * 2; - y3 = background_area.get_y() + background_area.get_height() / 2 - len; - } else { - x1 = background_area.get_x() + offset; - y1 = background_area.get_y() + background_area.get_height() / 2 - len; - x2 = background_area.get_x() + offset + len * 2; - y2 = background_area.get_y() + background_area.get_height() / 2; - x3 = background_area.get_x() + offset; - 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); - const auto expander_color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelsExpanderColor); - cr->set_source_rgb(expander_color.get_red(), expander_color.get_green(), expander_color.get_blue()); - cr->stroke(); -} - -// voice participant - -void CellRendererChannels::get_preferred_width_vfunc_voice_participant(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_voice_participant(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_voice_participant(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_voice_participant(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_voice_participant(const Cairo::RefPtr &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 = 0; - int pixbuf_h = 0; - - 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() + 28; - 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.property_scale() = Pango::SCALE_SMALL; - m_renderer_text.render(cr, widget, background_area, text_cell_area, flags); - m_renderer_text.property_scale_set() = false; - - 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(); - } - - auto *paned = dynamic_cast(widget.get_ancestor(Gtk::Paned::get_type())); - if (paned != nullptr) { - const auto edge = std::min(paned->get_position(), background_area.get_width()); - - const static std::array, 3> icon_order = { { - { VoiceStateFlags::SelfMute | VoiceStateFlags::Mute, "microphone-disabled-symbolic" }, - { VoiceStateFlags::SelfDeaf | VoiceStateFlags::Deaf, "audio-volume-muted-symbolic" }, - { VoiceStateFlags::SelfVideo, "camera-web-symbolic" }, - } }; - - constexpr static int IconSize = 18; - constexpr static int IconPad = 2; - - const VoiceStateFlags voice_flags = m_property_voice_state.get_value(); - - int offset = 0; - for (auto iter = icon_order.rbegin(); iter != icon_order.rend(); iter++) { - const auto &[flag, icon] = *iter; - if ((voice_flags & flag) == VoiceStateFlags::Clear) continue; - - const double icon_w = 18; - const double icon_h = 18; - const double icon_x = background_area.get_x() + edge - icon_w + offset; - const double icon_y = background_area.get_y() + background_area.get_height() / 2 - icon_h / 2; - Gdk::Rectangle icon_cell_area(icon_x, icon_y, icon_w, icon_h); - - offset -= (IconSize + IconPad); - - const bool is_server_mute = (voice_flags & VoiceStateFlags::Mute) == VoiceStateFlags::Mute; - const bool is_server_deaf = (voice_flags & VoiceStateFlags::Deaf) == VoiceStateFlags::Deaf; - auto context = widget.get_style_context(); - if (is_server_mute || is_server_deaf) { - context->context_save(); - context->add_class("voice-state-server"); - } - - m_renderer_pixbuf.property_icon_name() = icon; - m_renderer_pixbuf.render(cr, widget, background_area, icon_cell_area, flags); - - if (is_server_mute || is_server_deaf) { - context->context_restore(); - } - } - } -} - -#endif - -// 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 &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); - - if (!Abaddon::Get().GetSettings().Unreads) return; - - auto *paned = dynamic_cast(widget.get_ancestor(Gtk::Paned::get_type())); - if (paned != nullptr) { - const auto edge = std::min(paned->get_position(), background_area.get_width()); - if (const auto unread = Abaddon::Get().GetDiscordClient().GetUnreadDMsCount(); unread > 0) - unread_render_mentions(cr, widget, unread, edge, background_area); - } -} - -// 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 &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() + 3; - 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 + 6.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(static_cast(text_x), - static_cast(text_y), - static_cast(text_w), - static_cast(text_h)); - - auto &discord = Abaddon::Get().GetDiscordClient(); - const auto id = m_property_id.get_value(); - const bool is_muted = discord.IsChannelMuted(id); - - static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelColor); - if (Abaddon::Get().GetDiscordClient().IsChannelMuted(m_property_id.get_value())) { - auto muted = color; - muted.set_red(muted.get_red() * 0.5); - muted.set_green(muted.get_green() * 0.5); - muted.set_blue(muted.get_blue() * 0.5); - m_renderer_text.property_foreground_rgba() = muted; - } else { - m_renderer_text.property_foreground_rgba() = color; - } - m_renderer_text.render(cr, widget, background_area, text_cell_area, flags); - m_renderer_text.property_foreground_set() = false; - - 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(); - - // unread - if (!Abaddon::Get().GetSettings().Unreads) return; - - const auto unread_state = discord.GetUnreadStateForChannel(id); - if (unread_state < 0) return; - - if (!is_muted) { - static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().UnreadIndicatorColor); - cr->set_source_rgb(color.get_red(), color.get_green(), color.get_blue()); - const auto x = background_area.get_x(); - const auto y = background_area.get_y(); - const auto w = background_area.get_width(); - const auto h = background_area.get_height(); - cr->rectangle(x, y, 3, h); - cr->fill(); - } -} - -void CellRendererChannels::cairo_path_rounded_rect(const Cairo::RefPtr &cr, double x, double y, double w, double h, double r) { - const double degrees = M_PI / 180.0; - - cr->begin_new_sub_path(); - cr->arc(x + w - r, y + r, r, -M_PI_H, 0); - cr->arc(x + w - r, y + h - r, r, 0, M_PI_H); - cr->arc(x + r, y + h - r, r, M_PI_H, M_PI); - cr->arc(x + r, y + r, r, M_PI, M_PI_3_2); - cr->close_path(); -} - -void CellRendererChannels::unread_render_mentions(const Cairo::RefPtr &cr, Gtk::Widget &widget, int mentions, int edge, const Gdk::Rectangle &cell_area) { - Pango::FontDescription font; - font.set_family("sans 14"); - // font.set_weight(Pango::WEIGHT_BOLD); - - auto layout = widget.create_pango_layout(std::to_string(mentions)); - layout->set_font_description(font); - layout->set_alignment(Pango::ALIGN_RIGHT); - - int width, height; - layout->get_pixel_size(width, height); - { - static const auto bg = Gdk::RGBA(Abaddon::Get().GetSettings().MentionBadgeColor); - static const auto text = Gdk::RGBA(Abaddon::Get().GetSettings().MentionBadgeTextColor); - - const auto x = cell_area.get_x() + edge - width - MentionsRightPad; - const auto y = cell_area.get_y() + cell_area.get_height() / 2.0 - height / 2.0 - 1; - cairo_path_rounded_rect(cr, x - 4, y + 2, width + 8, height, 5); - cr->set_source_rgb(bg.get_red(), bg.get_green(), bg.get_blue()); - cr->fill(); - cr->set_source_rgb(text.get_red(), text.get_green(), text.get_blue()); - cr->move_to(x, y); - layout->show_in_cairo_context(cr); - } -} diff --git a/src/components/channelscellrenderer.hpp b/src/components/channelscellrenderer.hpp deleted file mode 100644 index 934ce5b..0000000 --- a/src/components/channelscellrenderer.hpp +++ /dev/null @@ -1,174 +0,0 @@ -#pragma once -#include -#include -#include -#include "discord/snowflake.hpp" -#include "discord/voicestateflags.hpp" -#include "misc/bitwise.hpp" - -enum class RenderType : uint8_t { - Folder, - Guild, - Category, - TextChannel, - Thread, - -// TODO: maybe enable anyways but without ability to join if no voice support -#ifdef WITH_VOICE - VoiceChannel, - VoiceParticipant, -#endif - - DMHeader, - DM, -}; - -class CellRendererChannels : public Gtk::CellRenderer { -public: - CellRendererChannels(); - ~CellRendererChannels() override = default; - - Glib::PropertyProxy property_type(); - Glib::PropertyProxy property_id(); - Glib::PropertyProxy property_name(); - Glib::PropertyProxy> property_icon(); - Glib::PropertyProxy> property_icon_animation(); - Glib::PropertyProxy property_expanded(); - Glib::PropertyProxy property_nsfw(); - Glib::PropertyProxy> property_color(); - Glib::PropertyProxy property_voice_state(); - -protected: - void get_preferred_width_vfunc(Gtk::Widget &widget, int &minimum_width, int &natural_width) const override; - void get_preferred_width_for_height_vfunc(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const override; - void get_preferred_height_vfunc(Gtk::Widget &widget, int &minimum_height, int &natural_height) const override; - void get_preferred_height_for_width_vfunc(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const override; - void render_vfunc(const Cairo::RefPtr &cr, - Gtk::Widget &widget, - const Gdk::Rectangle &background_area, - const Gdk::Rectangle &cell_area, - Gtk::CellRendererState flags) override; - - // guild functions - void get_preferred_width_vfunc_folder(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; - void get_preferred_width_for_height_vfunc_folder(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; - void get_preferred_height_vfunc_folder(Gtk::Widget &widget, int &minimum_height, int &natural_height) const; - void get_preferred_height_for_width_vfunc_folder(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const; - void render_vfunc_folder(const Cairo::RefPtr &cr, - Gtk::Widget &widget, - const Gdk::Rectangle &background_area, - const Gdk::Rectangle &cell_area, - Gtk::CellRendererState flags); - - // guild functions - void get_preferred_width_vfunc_guild(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; - void get_preferred_width_for_height_vfunc_guild(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; - void get_preferred_height_vfunc_guild(Gtk::Widget &widget, int &minimum_height, int &natural_height) const; - void get_preferred_height_for_width_vfunc_guild(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const; - void render_vfunc_guild(const Cairo::RefPtr &cr, - Gtk::Widget &widget, - const Gdk::Rectangle &background_area, - const Gdk::Rectangle &cell_area, - Gtk::CellRendererState flags); - - // category - void get_preferred_width_vfunc_category(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; - void get_preferred_width_for_height_vfunc_category(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; - void get_preferred_height_vfunc_category(Gtk::Widget &widget, int &minimum_height, int &natural_height) const; - void get_preferred_height_for_width_vfunc_category(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const; - void render_vfunc_category(const Cairo::RefPtr &cr, - Gtk::Widget &widget, - const Gdk::Rectangle &background_area, - const Gdk::Rectangle &cell_area, - Gtk::CellRendererState flags); - - // text channel - void get_preferred_width_vfunc_channel(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; - void get_preferred_width_for_height_vfunc_channel(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; - void get_preferred_height_vfunc_channel(Gtk::Widget &widget, int &minimum_height, int &natural_height) const; - void get_preferred_height_for_width_vfunc_channel(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const; - void render_vfunc_channel(const Cairo::RefPtr &cr, - Gtk::Widget &widget, - const Gdk::Rectangle &background_area, - const Gdk::Rectangle &cell_area, - Gtk::CellRendererState flags); - - // thread - void get_preferred_width_vfunc_thread(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; - void get_preferred_width_for_height_vfunc_thread(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; - void get_preferred_height_vfunc_thread(Gtk::Widget &widget, int &minimum_height, int &natural_height) const; - void get_preferred_height_for_width_vfunc_thread(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const; - void render_vfunc_thread(const Cairo::RefPtr &cr, - Gtk::Widget &widget, - const Gdk::Rectangle &background_area, - const Gdk::Rectangle &cell_area, - Gtk::CellRendererState flags); - -#ifdef WITH_VOICE - // voice channel - void get_preferred_width_vfunc_voice_channel(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; - void get_preferred_width_for_height_vfunc_voice_channel(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; - void get_preferred_height_vfunc_voice_channel(Gtk::Widget &widget, int &minimum_height, int &natural_height) const; - void get_preferred_height_for_width_vfunc_voice_channel(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const; - void render_vfunc_voice_channel(const Cairo::RefPtr &cr, - Gtk::Widget &widget, - const Gdk::Rectangle &background_area, - const Gdk::Rectangle &cell_area, - Gtk::CellRendererState flags); - - // voice participant - void get_preferred_width_vfunc_voice_participant(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; - void get_preferred_width_for_height_vfunc_voice_participant(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; - void get_preferred_height_vfunc_voice_participant(Gtk::Widget &widget, int &minimum_height, int &natural_height) const; - void get_preferred_height_for_width_vfunc_voice_participant(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const; - void render_vfunc_voice_participant(const Cairo::RefPtr &cr, - Gtk::Widget &widget, - const Gdk::Rectangle &background_area, - const Gdk::Rectangle &cell_area, - Gtk::CellRendererState flags); -#endif - - // dm header - void get_preferred_width_vfunc_dmheader(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; - void get_preferred_width_for_height_vfunc_dmheader(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; - void get_preferred_height_vfunc_dmheader(Gtk::Widget &widget, int &minimum_height, int &natural_height) const; - void get_preferred_height_for_width_vfunc_dmheader(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const; - void render_vfunc_dmheader(const Cairo::RefPtr &cr, - Gtk::Widget &widget, - const Gdk::Rectangle &background_area, - const Gdk::Rectangle &cell_area, - Gtk::CellRendererState flags); - - // dm - void get_preferred_width_vfunc_dm(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; - void get_preferred_width_for_height_vfunc_dm(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; - void get_preferred_height_vfunc_dm(Gtk::Widget &widget, int &minimum_height, int &natural_height) const; - void get_preferred_height_for_width_vfunc_dm(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const; - void render_vfunc_dm(const Cairo::RefPtr &cr, - Gtk::Widget &widget, - const Gdk::Rectangle &background_area, - const Gdk::Rectangle &cell_area, - Gtk::CellRendererState flags); - - static void cairo_path_rounded_rect(const Cairo::RefPtr &cr, double x, double y, double w, double h, double r); - static void unread_render_mentions(const Cairo::RefPtr &cr, Gtk::Widget &widget, int mentions, int edge, const Gdk::Rectangle &cell_area); - -private: - Gtk::CellRendererText m_renderer_text; - Gtk::CellRendererPixbuf m_renderer_pixbuf; - - Glib::Property m_property_type; // all - Glib::Property m_property_name; // all - Glib::Property m_property_id; - Glib::Property> m_property_pixbuf; // guild, dm - Glib::Property> m_property_pixbuf_animation; // guild - Glib::Property m_property_expanded; // category - Glib::Property m_property_nsfw; // channel - Glib::Property> m_property_color; // folder - Glib::Property m_property_voice_state; - - // same pitfalls as in https://github.com/uowuo/abaddon/blob/60404783bd4ce9be26233fe66fc3a74475d9eaa3/components/cellrendererpixbufanimation.hpp#L32-L39 - // this will manifest though since guild icons can change - // an animation or two wont be the end of the world though - std::map, Glib::RefPtr> m_pixbuf_anim_iters; -}; -- cgit v1.2.3