diff options
17 files changed, 728 insertions, 185 deletions
@@ -291,23 +291,24 @@ For example, memory_db would be set by adding `memory_db = true` under the line #### gui -| Setting | Type | Default | Description | -|-----------------------------|---------|---------|----------------------------------------------------------------------------------------------------------------------------| -| `member_list_discriminator` | boolean | true | show user discriminators in the member list | -| `stock_emojis` | boolean | true | allow abaddon to substitute unicode emojis with images from emojis.bin, must be false to allow GTK to render emojis itself | -| `custom_emojis` | boolean | true | download and use custom Discord emojis | -| `css` | string | | path to the main CSS file | -| `animations` | boolean | true | use animated images where available (e.g. server icons, emojis, avatars). false means static images will be used | -| `animated_guild_hover_only` | boolean | true | only animate guild icons when the guild is being hovered over | -| `owner_crown` | boolean | true | show a crown next to the owner | -| `unreads` | boolean | true | show unread indicators and mention badges | -| `save_state` | boolean | true | save the state of the gui (active channels, tabs, expanded channels) | -| `alt_menu` | boolean | false | keep the menu hidden unless revealed with alt key | -| `hide_to_tray` | boolean | false | hide abaddon to the system tray on window close | -| `show_deleted_indicator` | boolean | true | show \[deleted\] indicator next to deleted messages instead of actually deleting the message | -| `font_scale` | double | | scale font rendering. 1 is unchanged | -| `image_embed_clamp_width` | int | 400 | maximum width of image embeds | -| `image_embed_clamp_height` | int | 300 | maximum height of image embeds | +| Setting | Type | Default | Description | +|--------------------------------|---------|---------|----------------------------------------------------------------------------------------------------------------------------| +| `member_list_discriminator` | boolean | true | show user discriminators in the member list | +| `stock_emojis` | boolean | true | allow abaddon to substitute unicode emojis with images from emojis.bin, must be false to allow GTK to render emojis itself | +| `custom_emojis` | boolean | true | download and use custom Discord emojis | +| `css` | string | | path to the main CSS file | +| `animations` | boolean | true | use animated images where available (e.g. server icons, emojis, avatars). false means static images will be used | +| `animated_guild_hover_only` | boolean | true | only animate guild icons when the guild is being hovered over | +| `owner_crown` | boolean | true | show a crown next to the owner | +| `unreads` | boolean | true | show unread indicators and mention badges | +| `save_state` | boolean | true | save the state of the gui (active channels, tabs, expanded channels) | +| `alt_menu` | boolean | false | keep the menu hidden unless revealed with alt key | +| `hide_to_tray` | boolean | false | hide abaddon to the system tray on window close | +| `show_deleted_indicator` | boolean | true | show \[deleted\] indicator next to deleted messages instead of actually deleting the message | +| `font_scale` | double | | scale font rendering. 1 is unchanged | +| `image_embed_clamp_width` | int | 400 | maximum width of image embeds | +| `image_embed_clamp_height` | int | 300 | maximum height of image embeds | +| `classic_change_guild_on_open` | boolean | true | change displayed guild when selecting a channel (classic channel list) | #### style diff --git a/src/components/channelscellrenderer.cpp b/src/components/channellist/cellrendererchannels.cpp index bf84a9b..3db136b 100644 --- a/src/components/channelscellrenderer.cpp +++ b/src/components/channellist/cellrendererchannels.cpp @@ -1,4 +1,4 @@ -#include "channelscellrenderer.hpp" +#include "cellrendererchannels.hpp" constexpr static int MentionsRightPad = 7; #ifndef M_PI diff --git a/src/components/channelscellrenderer.hpp b/src/components/channellist/cellrendererchannels.hpp index 934ce5b..934ce5b 100644 --- a/src/components/channelscellrenderer.hpp +++ b/src/components/channellist/cellrendererchannels.hpp diff --git a/src/components/channellist/channellist.cpp b/src/components/channellist/channellist.cpp new file mode 100644 index 0000000..873de41 --- /dev/null +++ b/src/components/channellist/channellist.cpp @@ -0,0 +1,117 @@ +#include "channellist.hpp" + +ChannelList::ChannelList() { + ConnectSignals(); + + m_guilds.set_halign(Gtk::ALIGN_START); + + m_guilds_scroll.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); + + m_guilds.signal_guild_selected().connect([this](Snowflake guild_id) { + m_tree.SetSelectedGuild(guild_id); + }); + + m_guilds.signal_dms_selected().connect([this]() { + m_tree.SetSelectedDMs(); + }); + + m_guilds.show(); + m_tree.show(); + m_guilds_scroll.add(m_guilds); + pack_start(m_guilds_scroll, false, false); // only take the space it needs + pack_start(m_tree, true, true); // use all the remaining space +} + +void ChannelList::UpdateListing() { + m_tree.UpdateListing(); + m_guilds.UpdateListing(); +} + +void ChannelList::SetActiveChannel(Snowflake id, bool expand_to) { + if (Abaddon::Get().GetSettings().ClassicChangeGuildOnOpen) { + if (const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id); channel.has_value() && channel->GuildID.has_value()) { + m_tree.SetSelectedGuild(*channel->GuildID); + } else { + m_tree.SetSelectedDMs(); + } + } + + m_tree.SetActiveChannel(id, expand_to); +} + +void ChannelList::UseExpansionState(const ExpansionStateRoot &state) { + m_tree.UseExpansionState(state); +} + +ExpansionStateRoot ChannelList::GetExpansionState() const { + return m_tree.GetExpansionState(); +} + +void ChannelList::UsePanedHack(Gtk::Paned &paned) { + m_tree.UsePanedHack(paned); +} + +void ChannelList::SetClassic(bool value) { + m_tree.SetClassic(value); + m_guilds_scroll.set_visible(value); +} + +void ChannelList::ConnectSignals() { + // TODO: if these all just travel upwards to the singleton then get rid of them but mayeb they dont + +#ifdef WITH_LIBHANDY + m_tree.signal_action_open_new_tab().connect([this](Snowflake id) { + m_signal_action_open_new_tab.emit(id); + }); +#endif + +#ifdef WITH_VOICE + m_tree.signal_action_join_voice_channel().connect([this](Snowflake id) { + m_signal_action_join_voice_channel.emit(id); + }); + + m_tree.signal_action_disconnect_voice().connect([this]() { + m_signal_action_disconnect_voice.emit(); + }); +#endif + + m_tree.signal_action_channel_item_select().connect([this](Snowflake id) { + m_signal_action_channel_item_select.emit(id); + }); + + m_tree.signal_action_guild_leave().connect([this](Snowflake id) { + m_signal_action_guild_leave.emit(id); + }); + + m_tree.signal_action_guild_settings().connect([this](Snowflake id) { + m_signal_action_guild_settings.emit(id); + }); +} + +#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::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; +} diff --git a/src/components/channellist/channellist.hpp b/src/components/channellist/channellist.hpp new file mode 100644 index 0000000..692afa7 --- /dev/null +++ b/src/components/channellist/channellist.hpp @@ -0,0 +1,69 @@ +#pragma once +#include <gtkmm/box.h> +#include <gtkmm/paned.h> +#include "channellisttree.hpp" +#include "classic/guildlist.hpp" +#include "discord/snowflake.hpp" +#include "state.hpp" + +// Contains the actual ChannelListTree and the classic listing if enabled +class ChannelList : public Gtk::HBox { + // have to proxy public and signals to underlying tree... ew!!! +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); + + void SetClassic(bool value); + +private: + void ConnectSignals(); + + ChannelListTree m_tree; + + Gtk::ScrolledWindow m_guilds_scroll; + GuildList m_guilds; + +public: + using type_signal_action_channel_item_select = sigc::signal<void, Snowflake>; + using type_signal_action_guild_leave = sigc::signal<void, Snowflake>; + using type_signal_action_guild_settings = sigc::signal<void, Snowflake>; + +#ifdef WITH_LIBHANDY + using type_signal_action_open_new_tab = sigc::signal<void, Snowflake>; + type_signal_action_open_new_tab signal_action_open_new_tab(); +#endif + +#ifdef WITH_VOICE + using type_signal_action_join_voice_channel = sigc::signal<void, Snowflake>; + using type_signal_action_disconnect_voice = sigc::signal<void>; + + 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/channels.cpp b/src/components/channellist/channellisttree.cpp index 6ed3ca4..cbfc8bb 100644 --- a/src/components/channels.cpp +++ b/src/components/channellist/channellisttree.cpp @@ -1,12 +1,13 @@ -#include "channels.hpp" +#include "channellisttree.hpp" #include "imgmanager.hpp" #include <algorithm> #include <map> #include <unordered_map> -ChannelList::ChannelList() - : Glib::ObjectBase(typeid(ChannelList)) +ChannelListTree::ChannelListTree() + : Glib::ObjectBase(typeid(ChannelListTree)) , 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) @@ -35,9 +36,9 @@ ChannelList::ChannelList() , m_menu_thread_mark_as_read("Mark as _Read", true) { get_style_context()->add_class("channel-list"); - // todo: move to method + // Filter iters const auto cb = [this](const Gtk::TreeModel::Path &path, Gtk::TreeViewColumn *column) { - auto row = *m_model->get_iter(path); + 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 @@ -58,12 +59,12 @@ ChannelList::ChannelList() } }; 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.signal_row_collapsed().connect(sigc::mem_fun(*this, &ChannelListTree::OnRowCollapsed), false); + m_view.signal_row_expanded().connect(sigc::mem_fun(*this, &ChannelListTree::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.get_selection()->set_select_function(sigc::mem_fun(*this, &ChannelListTree::SelectionFunc)); + m_view.signal_button_press_event().connect(sigc::mem_fun(*this, &ChannelListTree::OnButtonPressEvent), false); m_view.set_hexpand(true); m_view.set_vexpand(true); @@ -71,13 +72,31 @@ ChannelList::ChannelList() 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_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]) - m_view.expand_row(m_model->get_path(parent), false); + 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 (!m_classic) return true; + + const RenderType type = (*iter)[m_columns.m_type]; + + if (m_classic_selected_dms) { + if (iter->parent()) return true; + return type == RenderType::DMHeader; + } + + if (type == RenderType::Guild) { + return (*iter)[m_columns.m_id] == m_classic_selected_guild; + } + return type != RenderType::DMHeader; }); m_view.show(); @@ -261,40 +280,90 @@ ChannelList::ChannelList() 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)); + discord.signal_message_create().connect(sigc::mem_fun(*this, &ChannelListTree::OnMessageCreate)); + discord.signal_guild_create().connect(sigc::mem_fun(*this, &ChannelListTree::UpdateNewGuild)); + discord.signal_guild_delete().connect(sigc::mem_fun(*this, &ChannelListTree::UpdateRemoveGuild)); + discord.signal_channel_delete().connect(sigc::mem_fun(*this, &ChannelListTree::UpdateRemoveChannel)); + discord.signal_channel_update().connect(sigc::mem_fun(*this, &ChannelListTree::UpdateChannel)); + discord.signal_channel_create().connect(sigc::mem_fun(*this, &ChannelListTree::UpdateCreateChannel)); + discord.signal_thread_delete().connect(sigc::mem_fun(*this, &ChannelListTree::OnThreadDelete)); + discord.signal_thread_update().connect(sigc::mem_fun(*this, &ChannelListTree::OnThreadUpdate)); + discord.signal_thread_list_sync().connect(sigc::mem_fun(*this, &ChannelListTree::OnThreadListSync)); + discord.signal_added_to_thread().connect(sigc::mem_fun(*this, &ChannelListTree::OnThreadJoined)); + discord.signal_removed_from_thread().connect(sigc::mem_fun(*this, &ChannelListTree::OnThreadRemoved)); + discord.signal_guild_update().connect(sigc::mem_fun(*this, &ChannelListTree::UpdateGuild)); + discord.signal_message_ack().connect(sigc::mem_fun(*this, &ChannelListTree::OnMessageAck)); + discord.signal_channel_muted().connect(sigc::mem_fun(*this, &ChannelListTree::OnChannelMute)); + discord.signal_channel_unmuted().connect(sigc::mem_fun(*this, &ChannelListTree::OnChannelUnmute)); + discord.signal_guild_muted().connect(sigc::mem_fun(*this, &ChannelListTree::OnGuildMute)); + discord.signal_guild_unmuted().connect(sigc::mem_fun(*this, &ChannelListTree::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)); + discord.signal_voice_user_connect().connect(sigc::mem_fun(*this, &ChannelListTree::OnVoiceUserConnect)); + discord.signal_voice_user_disconnect().connect(sigc::mem_fun(*this, &ChannelListTree::OnVoiceUserDisconnect)); + discord.signal_voice_state_set().connect(sigc::mem_fun(*this, &ChannelListTree::OnVoiceStateSet)); #endif } -void ChannelList::UsePanedHack(Gtk::Paned &paned) { - paned.property_position().signal_changed().connect(sigc::mem_fun(*this, &ChannelList::OnPanedPositionChanged)); +void ChannelListTree::UsePanedHack(Gtk::Paned &paned) { + paned.property_position().signal_changed().connect(sigc::mem_fun(*this, &ChannelListTree::OnPanedPositionChanged)); +} + +void ChannelListTree::SetClassic(bool value) { + m_classic = value; + m_filter_model->refilter(); +} + +void ChannelListTree::SetSelectedGuild(Snowflake guild_id) { + m_classic_selected_guild = guild_id; + m_classic_selected_dms = false; + m_filter_model->refilter(); + auto guild_iter = GetIteratorForGuildFromID(guild_id); + if (guild_iter) { + if (auto filter_iter = m_filter_model->convert_child_iter_to_iter(guild_iter)) { + m_view.expand_row(m_filter_model->get_path(filter_iter), false); + } + } +} + +void ChannelListTree::SetSelectedDMs() { + m_classic_selected_dms = true; + m_filter_model->refilter(); + if (m_dm_header) { + if (auto filter_path = m_filter_model->convert_child_path_to_path(m_dm_header)) { + m_view.expand_row(filter_path, false); + } + } } -void ChannelList::OnPanedPositionChanged() { +void ChannelListTree::OnPanedPositionChanged() { m_view.queue_draw(); } -void ChannelList::UpdateListing() { +void ChannelListTree::UpdateListingClassic() { + m_updating_listing = true; + + m_model->clear(); + + auto &discord = Abaddon::Get().GetDiscordClient(); + const auto guild_ids = discord.GetUserSortedGuilds(); + for (const auto guild_id : guild_ids) { + if (const auto guild = discord.GetGuild(guild_id); guild.has_value()) { + AddGuild(*guild, m_model->children()); + } + } + + m_updating_listing = false; + + AddPrivateChannels(); +} + +void ChannelListTree::UpdateListing() { + if (m_classic) { + UpdateListingClassic(); + return; + } + m_updating_listing = true; m_model->clear(); @@ -364,7 +433,7 @@ void ChannelList::UpdateListing() { } // TODO update for folders -void ChannelList::UpdateNewGuild(const GuildData &guild) { +void ChannelListTree::UpdateNewGuild(const GuildData &guild) { AddGuild(guild, m_model->children()); // update sort order int sortnum = 0; @@ -375,19 +444,19 @@ void ChannelList::UpdateNewGuild(const GuildData &guild) { } } -void ChannelList::UpdateRemoveGuild(Snowflake id) { +void ChannelListTree::UpdateRemoveGuild(Snowflake id) { auto iter = GetIteratorForGuildFromID(id); if (!iter) return; m_model->erase(iter); } -void ChannelList::UpdateRemoveChannel(Snowflake id) { +void ChannelListTree::UpdateRemoveChannel(Snowflake id) { auto iter = GetIteratorForRowFromID(id); if (!iter) return; m_model->erase(iter); } -void ChannelList::UpdateChannel(Snowflake id) { +void ChannelListTree::UpdateChannel(Snowflake id) { auto iter = GetIteratorForRowFromID(id); auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id); if (!iter || !channel.has_value()) return; @@ -411,7 +480,7 @@ void ChannelList::UpdateChannel(Snowflake id) { MoveRow(iter, new_parent); } -void ChannelList::UpdateCreateChannel(const ChannelData &channel) { +void ChannelListTree::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; @@ -437,7 +506,7 @@ void ChannelList::UpdateCreateChannel(const ChannelData &channel) { channel_row[m_columns.m_sort] = *channel.Position; } -void ChannelList::UpdateGuild(Snowflake id) { +void ChannelListTree::UpdateGuild(Snowflake id) { auto iter = GetIteratorForGuildFromID(id); auto &img = Abaddon::Get().GetImageManager(); const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(id); @@ -461,7 +530,7 @@ void ChannelList::UpdateGuild(Snowflake id) { } } -void ChannelList::OnThreadJoined(Snowflake id) { +void ChannelListTree::OnThreadJoined(Snowflake id) { if (GetIteratorForRowFromID(id)) return; const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id); if (!channel.has_value()) return; @@ -470,16 +539,16 @@ void ChannelList::OnThreadJoined(Snowflake id) { CreateThreadRow(parent->children(), *channel); } -void ChannelList::OnThreadRemoved(Snowflake id) { +void ChannelListTree::OnThreadRemoved(Snowflake id) { DeleteThreadRow(id); } -void ChannelList::OnThreadDelete(const ThreadDeleteData &data) { +void ChannelListTree::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) { +void ChannelListTree::OnThreadUpdate(const ThreadUpdateData &data) { auto iter = GetIteratorForRowFromID(data.Thread.ID); if (iter) (*iter)[m_columns.m_name] = "- " + Glib::Markup::escape_text(*data.Thread.Name); @@ -488,7 +557,7 @@ void ChannelList::OnThreadUpdate(const ThreadUpdateData &data) { DeleteThreadRow(data.Thread.ID); } -void ChannelList::OnThreadListSync(const ThreadListSyncData &data) { +void ChannelListTree::OnThreadListSync(const ThreadListSyncData &data) { // get the threads in the guild std::vector<Snowflake> threads; auto guild_iter = GetIteratorForGuildFromID(data.GuildID); @@ -524,7 +593,7 @@ void ChannelList::OnThreadListSync(const ThreadListSyncData &data) { } #ifdef WITH_VOICE -void ChannelList::OnVoiceUserConnect(Snowflake user_id, Snowflake channel_id) { +void ChannelListTree::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; @@ -534,48 +603,50 @@ void ChannelList::OnVoiceUserConnect(Snowflake user_id, Snowflake channel_id) { CreateVoiceParticipantRow(*user, parent_iter->children()); } -void ChannelList::OnVoiceUserDisconnect(Snowflake user_id, Snowflake channel_id) { +void ChannelListTree::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) { +void ChannelListTree::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) { +void ChannelListTree::DeleteThreadRow(Snowflake id) { auto iter = GetIteratorForRowFromID(id); if (iter) m_model->erase(iter); } -void ChannelList::OnChannelMute(Snowflake id) { +void ChannelListTree::OnChannelMute(Snowflake id) { if (auto iter = GetIteratorForRowFromID(id)) m_model->row_changed(m_model->get_path(iter), iter); } -void ChannelList::OnChannelUnmute(Snowflake id) { +void ChannelListTree::OnChannelUnmute(Snowflake id) { if (auto iter = GetIteratorForRowFromID(id)) m_model->row_changed(m_model->get_path(iter), iter); } -void ChannelList::OnGuildMute(Snowflake id) { +void ChannelListTree::OnGuildMute(Snowflake id) { if (auto iter = GetIteratorForGuildFromID(id)) m_model->row_changed(m_model->get_path(iter), iter); } -void ChannelList::OnGuildUnmute(Snowflake id) { +void ChannelListTree::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) { +void ChannelListTree::SetActiveChannel(Snowflake id, bool expand_to) { + while (Gtk::Main::events_pending()) Gtk::Main::iteration(); + // mark channel as read when switching off if (m_active_channel.IsValid()) Abaddon::Get().GetDiscordClient().MarkChannelAsRead(m_active_channel, [](...) {}); @@ -592,10 +663,14 @@ void ChannelList::SetActiveChannel(Snowflake id, bool expand_to) { 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()->unselect_all(); + const auto filter_iter = m_filter_model->convert_child_iter_to_iter(channel_iter); + if (filter_iter) { + if (expand_to) { + m_view.expand_to_path(m_filter_model->get_path(filter_iter)); + } + m_view.get_selection()->select(filter_iter); } - m_view.get_selection()->select(channel_iter); } else { m_view.get_selection()->unselect_all(); const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id); @@ -603,68 +678,23 @@ void ChannelList::SetActiveChannel(Snowflake id, bool expand_to) { 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)); + const auto filter_iter = m_filter_model->convert_child_iter_to_iter(m_temporary_thread_row); + if (filter_iter) { + m_view.get_selection()->select(filter_iter); } - - recurse(recurse, state.Children); } +} - m_tmp_row_map.clear(); +void ChannelListTree::UseExpansionState(const ExpansionStateRoot &root) { } -ExpansionStateRoot ChannelList::GetExpansionState() const { +ExpansionStateRoot ChannelListTree::GetExpansionState() const { ExpansionStateRoot r; - auto recurse = [this](auto &self, const Gtk::TreeRow &row) -> ExpansionState { - ExpansionState r; - - r.IsExpanded = row[m_columns.m_expanded]; - for (const auto &child : row.children()) - r.Children.Children[static_cast<Snowflake>(child[m_columns.m_id])] = self(self, child); - - return r; - }; - - for (const auto &child : m_model->children()) { - const auto id = static_cast<Snowflake>(child[m_columns.m_id]); - if (static_cast<uint64_t>(id) == 0ULL) continue; // dont save DM header - r.Children[id] = recurse(recurse, child); - } - return r; } -Gtk::TreeModel::iterator ChannelList::AddFolder(const UserSettingsGuildFoldersEntry &folder) { +Gtk::TreeModel::iterator ChannelListTree::AddFolder(const UserSettingsGuildFoldersEntry &folder) { if (!folder.ID.has_value()) { // just a guild if (!folder.GuildIDs.empty()) { @@ -677,7 +707,6 @@ Gtk::TreeModel::iterator ChannelList::AddFolder(const UserSettingsGuildFoldersEn 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 { @@ -702,7 +731,7 @@ Gtk::TreeModel::iterator ChannelList::AddFolder(const UserSettingsGuildFoldersEn return {}; } -Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild, const Gtk::TreeNodeChildren &root) { +Gtk::TreeModel::iterator ChannelListTree::AddGuild(const GuildData &guild, const Gtk::TreeNodeChildren &root) { auto &discord = Abaddon::Get().GetDiscordClient(); auto &img = Abaddon::Get().GetImageManager(); @@ -711,7 +740,6 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild, const Gtk guild_row[m_columns.m_id] = guild.ID; guild_row[m_columns.m_name] = "<b>" + Glib::Markup::escape_text(guild.Name) + "</b>"; guild_row[m_columns.m_icon] = img.GetPlaceholder(GuildIconSize); - 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<Gdk::PixbufAnimation> &pb) { @@ -762,8 +790,9 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild, const Gtk 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); + for (const auto &thread : it->second) { + CreateThreadRow(row.children(), thread); + } }; #ifdef WITH_VOICE @@ -793,7 +822,6 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild, const Gtk 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) { @@ -805,7 +833,6 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild, const Gtk 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) { @@ -825,14 +852,13 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild, const Gtk 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) { +Gtk::TreeModel::iterator ChannelListTree::UpdateCreateChannelCategory(const ChannelData &channel) { const auto iter = GetIteratorForGuildFromID(*channel.GuildID); if (!iter) return {}; @@ -846,7 +872,7 @@ Gtk::TreeModel::iterator ChannelList::UpdateCreateChannelCategory(const ChannelD return cat_row; } -Gtk::TreeModel::iterator ChannelList::CreateThreadRow(const Gtk::TreeNodeChildren &children, const ChannelData &channel) { +Gtk::TreeModel::iterator ChannelListTree::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; @@ -859,7 +885,7 @@ Gtk::TreeModel::iterator ChannelList::CreateThreadRow(const Gtk::TreeNodeChildre } #ifdef WITH_VOICE -Gtk::TreeModel::iterator ChannelList::CreateVoiceParticipantRow(const UserData &user, const Gtk::TreeNodeChildren &parent) { +Gtk::TreeModel::iterator ChannelListTree::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; @@ -882,7 +908,7 @@ Gtk::TreeModel::iterator ChannelList::CreateVoiceParticipantRow(const UserData & } #endif -void ChannelList::UpdateChannelCategory(const ChannelData &channel) { +void ChannelListTree::UpdateChannelCategory(const ChannelData &channel) { auto iter = GetIteratorForRowFromID(channel.ID); if (!iter) return; @@ -891,7 +917,7 @@ void ChannelList::UpdateChannelCategory(const ChannelData &channel) { } // todo this all needs refactoring for shooore -Gtk::TreeModel::iterator ChannelList::GetIteratorForTopLevelFromID(Snowflake id) { +Gtk::TreeModel::iterator ChannelListTree::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; @@ -906,7 +932,7 @@ Gtk::TreeModel::iterator ChannelList::GetIteratorForTopLevelFromID(Snowflake id) return {}; } -Gtk::TreeModel::iterator ChannelList::GetIteratorForGuildFromID(Snowflake id) { +Gtk::TreeModel::iterator ChannelListTree::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; @@ -921,7 +947,7 @@ Gtk::TreeModel::iterator ChannelList::GetIteratorForGuildFromID(Snowflake id) { return {}; } -Gtk::TreeModel::iterator ChannelList::GetIteratorForRowFromID(Snowflake id) { +Gtk::TreeModel::iterator ChannelListTree::GetIteratorForRowFromID(Snowflake id) { std::queue<Gtk::TreeModel::iterator> queue; for (const auto &child : m_model->children()) for (const auto &child2 : child.children()) @@ -938,7 +964,7 @@ Gtk::TreeModel::iterator ChannelList::GetIteratorForRowFromID(Snowflake id) { return {}; } -Gtk::TreeModel::iterator ChannelList::GetIteratorForRowFromIDOfType(Snowflake id, RenderType type) { +Gtk::TreeModel::iterator ChannelListTree::GetIteratorForRowFromIDOfType(Snowflake id, RenderType type) { std::queue<Gtk::TreeModel::iterator> queue; for (const auto &child : m_model->children()) for (const auto &child2 : child.children()) @@ -955,20 +981,20 @@ Gtk::TreeModel::iterator ChannelList::GetIteratorForRowFromIDOfType(Snowflake id return {}; } -bool ChannelList::IsTextChannel(ChannelType type) { +bool ChannelListTree::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 { +void ChannelListTree::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) { +void ChannelListTree::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); + m_view.expand_row(m_filter_model->get_path(it), false); } // try and restore selection if previous collapsed @@ -979,16 +1005,18 @@ void ChannelList::OnRowExpanded(const Gtk::TreeModel::iterator &iter, const Gtk: (*iter)[m_columns.m_expanded] = true; } -bool ChannelList::SelectionFunc(const Glib::RefPtr<Gtk::TreeModel> &model, const Gtk::TreeModel::Path &path, bool is_currently_selected) { - if (auto selection = m_view.get_selection()) - if (auto row = selection->get_selected()) - m_last_selected = m_model->get_path(row); +bool ChannelListTree::SelectionFunc(const Glib::RefPtr<Gtk::TreeModel> &model, const Gtk::TreeModel::Path &path, bool is_currently_selected) { + if (auto selection = m_view.get_selection()) { + if (auto row = selection->get_selected()) { + m_last_selected = m_filter_model->get_path(row); + } + } - auto type = (*m_model->get_iter(path))[m_columns.m_type]; + auto type = (*model->get_iter(path))[m_columns.m_type]; return type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread; } -void ChannelList::AddPrivateChannels() { +void ChannelListTree::AddPrivateChannels() { auto header_row = *m_model->append(); header_row[m_columns.m_type] = RenderType::DMHeader; header_row[m_columns.m_sort] = -1; @@ -1029,7 +1057,7 @@ void ChannelList::AddPrivateChannels() { } } -void ChannelList::UpdateCreateDMChannel(const ChannelData &dm) { +void ChannelListTree::UpdateCreateDMChannel(const ChannelData &dm) { auto header_row = m_model->get_iter(m_dm_header); auto &img = Abaddon::Get().GetImageManager(); @@ -1044,7 +1072,7 @@ void ChannelList::UpdateCreateDMChannel(const ChannelData &dm) { SetDMChannelIcon(iter, dm); } -void ChannelList::SetDMChannelIcon(Gtk::TreeIter iter, const ChannelData &dm) { +void ChannelListTree::SetDMChannelIcon(Gtk::TreeIter iter, const ChannelData &dm) { auto &img = Abaddon::Get().GetImageManager(); std::optional<UserData> top_recipient; @@ -1101,7 +1129,7 @@ void ChannelList::SetDMChannelIcon(Gtk::TreeIter iter, const ChannelData &dm) { } } -void ChannelList::RedrawUnreadIndicatorsForChannel(const ChannelData &channel) { +void ChannelListTree::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); @@ -1112,7 +1140,7 @@ void ChannelList::RedrawUnreadIndicatorsForChannel(const ChannelData &channel) { } } -void ChannelList::OnMessageAck(const MessageAckData &data) { +void ChannelListTree::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); @@ -1123,7 +1151,7 @@ void ChannelList::OnMessageAck(const MessageAckData &data) { } } -void ChannelList::OnMessageCreate(const Message &msg) { +void ChannelListTree::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); @@ -1135,9 +1163,11 @@ void ChannelList::OnMessageCreate(const Message &msg) { RedrawUnreadIndicatorsForChannel(*channel); } -bool ChannelList::OnButtonPressEvent(GdkEventButton *ev) { +bool ChannelListTree::OnButtonPressEvent(GdkEventButton *ev) { if (ev->button == GDK_BUTTON_SECONDARY && ev->type == GDK_BUTTON_PRESS) { if (m_view.get_path_at_pos(static_cast<int>(ev->x), static_cast<int>(ev->y), m_path_for_menu)) { + m_path_for_menu = m_filter_model->convert_path_to_child_path(m_path_for_menu); + if (!m_path_for_menu) return true; auto row = (*m_model->get_iter(m_path_for_menu)); switch (static_cast<RenderType>(row[m_columns.m_type])) { case RenderType::Guild: @@ -1182,7 +1212,7 @@ bool ChannelList::OnButtonPressEvent(GdkEventButton *ev) { return false; } -void ChannelList::MoveRow(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::iterator &new_parent) { +void ChannelListTree::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 @@ -1210,7 +1240,7 @@ void ChannelList::MoveRow(const Gtk::TreeModel::iterator &iter, const Gtk::TreeM m_model->erase(iter); } -void ChannelList::OnGuildSubmenuPopup() { +void ChannelListTree::OnGuildSubmenuPopup() { const auto iter = m_model->get_iter(m_path_for_menu); if (!iter) return; const auto id = static_cast<Snowflake>((*iter)[m_columns.m_id]); @@ -1225,7 +1255,7 @@ void ChannelList::OnGuildSubmenuPopup() { m_menu_guild_leave.set_sensitive(!(guild.has_value() && guild->OwnerID == self_id)); } -void ChannelList::OnCategorySubmenuPopup() { +void ChannelListTree::OnCategorySubmenuPopup() { const auto iter = m_model->get_iter(m_path_for_menu); if (!iter) return; const auto id = static_cast<Snowflake>((*iter)[m_columns.m_id]); @@ -1235,7 +1265,7 @@ void ChannelList::OnCategorySubmenuPopup() { m_menu_category_toggle_mute.set_label("Mute"); } -void ChannelList::OnChannelSubmenuPopup() { +void ChannelListTree::OnChannelSubmenuPopup() { const auto iter = m_model->get_iter(m_path_for_menu); if (!iter) return; const auto id = static_cast<Snowflake>((*iter)[m_columns.m_id]); @@ -1251,7 +1281,7 @@ void ChannelList::OnChannelSubmenuPopup() { } #ifdef WITH_VOICE -void ChannelList::OnVoiceChannelSubmenuPopup() { +void ChannelListTree::OnVoiceChannelSubmenuPopup() { const auto iter = m_model->get_iter(m_path_for_menu); if (!iter) return; const auto id = static_cast<Snowflake>((*iter)[m_columns.m_id]); @@ -1266,7 +1296,7 @@ void ChannelList::OnVoiceChannelSubmenuPopup() { } #endif -void ChannelList::OnDMSubmenuPopup() { +void ChannelListTree::OnDMSubmenuPopup() { auto iter = m_model->get_iter(m_path_for_menu); if (!iter) return; const auto id = static_cast<Snowflake>((*iter)[m_columns.m_id]); @@ -1287,7 +1317,7 @@ void ChannelList::OnDMSubmenuPopup() { #endif } -void ChannelList::OnThreadSubmenuPopup() { +void ChannelListTree::OnThreadSubmenuPopup() { m_menu_thread_archive.set_visible(false); m_menu_thread_unarchive.set_visible(false); @@ -1309,35 +1339,35 @@ void ChannelList::OnThreadSubmenuPopup() { m_menu_thread_unarchive.set_visible(channel->ThreadMetadata->IsArchived); } -ChannelList::type_signal_action_channel_item_select ChannelList::signal_action_channel_item_select() { +ChannelListTree::type_signal_action_channel_item_select ChannelListTree::signal_action_channel_item_select() { return m_signal_action_channel_item_select; } -ChannelList::type_signal_action_guild_leave ChannelList::signal_action_guild_leave() { +ChannelListTree::type_signal_action_guild_leave ChannelListTree::signal_action_guild_leave() { return m_signal_action_guild_leave; } -ChannelList::type_signal_action_guild_settings ChannelList::signal_action_guild_settings() { +ChannelListTree::type_signal_action_guild_settings ChannelListTree::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() { +ChannelListTree::type_signal_action_open_new_tab ChannelListTree::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() { +ChannelListTree::type_signal_action_join_voice_channel ChannelListTree::signal_action_join_voice_channel() { return m_signal_action_join_voice_channel; } -ChannelList::type_signal_action_disconnect_voice ChannelList::signal_action_disconnect_voice() { +ChannelListTree::type_signal_action_disconnect_voice ChannelListTree::signal_action_disconnect_voice() { return m_signal_action_disconnect_voice; } #endif -ChannelList::ModelColumns::ModelColumns() { +ChannelListTree::ModelColumns::ModelColumns() { add(m_type); add(m_id); add(m_name); diff --git a/src/components/channels.hpp b/src/components/channellist/channellisttree.hpp index 9d449e4..7ad0d29 100644 --- a/src/components/channels.hpp +++ b/src/components/channellist/channellisttree.hpp @@ -8,20 +8,21 @@ #include <gtkmm/scrolledwindow.h> #include <gtkmm/treemodel.h> #include <gtkmm/treestore.h> +#include <gtkmm/treemodelfilter.h> #include <gtkmm/treeview.h> #include <sigc++/sigc++.h> #include "discord/discord.hpp" #include "state.hpp" -#include "channelscellrenderer.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 { +class ChannelListTree : public Gtk::ScrolledWindow { public: - ChannelList(); + ChannelListTree(); void UpdateListing(); void SetActiveChannel(Snowflake id, bool expand_to); @@ -32,9 +33,15 @@ public: void UsePanedHack(Gtk::Paned &paned); + void SetClassic(bool value); + void SetSelectedGuild(Snowflake guild_id); + void SetSelectedDMs(); + protected: void OnPanedPositionChanged(); + void UpdateListingClassic(); + void UpdateNewGuild(const GuildData &guild); void UpdateRemoveGuild(Snowflake id); void UpdateRemoveChannel(Snowflake id); @@ -82,6 +89,7 @@ protected: ModelColumns m_columns; Glib::RefPtr<Gtk::TreeStore> m_model; + Glib::RefPtr<Gtk::TreeModelFilter> m_filter_model; Gtk::TreeModel::iterator AddFolder(const UserSettingsGuildFoldersEntry &folder); Gtk::TreeModel::iterator AddGuild(const GuildData &guild, const Gtk::TreeNodeChildren &root); @@ -116,7 +124,7 @@ protected: void UpdateCreateDMChannel(const ChannelData &channel); void SetDMChannelIcon(Gtk::TreeIter iter, const ChannelData &dm); - void RedrawUnreadIndicatorsForChannel(const ChannelData& channel); + void RedrawUnreadIndicatorsForChannel(const ChannelData &channel); void OnMessageAck(const MessageAckData &data); void OnMessageCreate(const Message &msg); @@ -184,12 +192,11 @@ protected: bool m_updating_listing = false; - Snowflake m_active_channel; + bool m_classic = false; + Snowflake m_classic_selected_guild; + bool m_classic_selected_dms = false; - // (GetIteratorForChannelFromID is rather slow) - // only temporary since i dont want to worry about maintaining this map - std::unordered_map<Snowflake, Gtk::TreeModel::iterator> m_tmp_row_map; - std::unordered_map<Snowflake, Gtk::TreeModel::iterator> m_tmp_guild_row_map; + Snowflake m_active_channel; public: using type_signal_action_channel_item_select = sigc::signal<void, Snowflake>; diff --git a/src/components/channellist/classic/guildlist.cpp b/src/components/channellist/classic/guildlist.cpp new file mode 100644 index 0000000..53ba1d7 --- /dev/null +++ b/src/components/channellist/classic/guildlist.cpp @@ -0,0 +1,117 @@ +#include "guildlist.hpp" +#include "guildlistfolderitem.hpp" + +class GuildListDMsButton : public Gtk::EventBox { +public: + GuildListDMsButton() { + set_size_request(48, 48); + + m_img.property_icon_name() = "user-available-symbolic"; // meh + m_img.property_icon_size() = Gtk::ICON_SIZE_DND; + add(m_img); + show_all_children(); + } + +private: + Gtk::Image m_img; +}; + +GuildList::GuildList() { + set_selection_mode(Gtk::SELECTION_NONE); + show_all_children(); +} + +void GuildList::UpdateListing() { + auto &discord = Abaddon::Get().GetDiscordClient(); + + Clear(); + + auto *dms = Gtk::make_managed<GuildListDMsButton>(); + dms->show(); + dms->signal_button_press_event().connect([this](GdkEventButton *ev) -> bool { + if (ev->type == GDK_BUTTON_PRESS && ev->button == GDK_BUTTON_PRIMARY) { + m_signal_dms_selected.emit(); + } + return false; + }); + add(*dms); + + // does this function still even work ??lol + const auto folders = discord.GetUserSettings().GuildFolders; + const auto guild_ids = discord.GetUserSortedGuilds(); + + // same logic from ChannelListTree + + std::set<Snowflake> foldered_guilds; + for (const auto &group : folders) { + foldered_guilds.insert(group.GuildIDs.begin(), group.GuildIDs.end()); + } + + for (auto iter = guild_ids.crbegin(); iter != guild_ids.crend(); iter++) { + if (foldered_guilds.find(*iter) == foldered_guilds.end()) { + AddGuild(*iter); + } + } + + for (const auto &group : folders) { + AddFolder(group); + } +} + +void GuildList::AddGuild(Snowflake id) { + if (auto item = CreateGuildWidget(id)) { + item->show(); + add(*item); + } +} + +GuildListGuildItem *GuildList::CreateGuildWidget(Snowflake id) { + const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(id); + if (!guild.has_value()) return nullptr; + + auto *item = Gtk::make_managed<GuildListGuildItem>(*guild); + item->signal_button_press_event().connect([this, id](GdkEventButton *event) -> bool { + if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_PRIMARY) { + m_signal_guild_selected.emit(id); + } + return true; + }); + + return item; +} + +void GuildList::AddFolder(const UserSettingsGuildFoldersEntry &folder) { + // groups with no ID arent actually folders + if (!folder.ID.has_value()) { + if (!folder.GuildIDs.empty()) { + AddGuild(folder.GuildIDs[0]); + } + return; + } + + auto *folder_widget = Gtk::make_managed<GuildListFolderItem>(folder); + for (const auto guild_id : folder.GuildIDs) { + if (auto *guild_widget = CreateGuildWidget(guild_id)) { + guild_widget->show(); + folder_widget->AddGuildWidget(guild_widget); + } + } + + folder_widget->show(); + add(*folder_widget); +} + +void GuildList::Clear() { + const auto children = get_children(); + for (auto *child : children) { + delete child; + } +} + +GuildList::type_signal_guild_selected GuildList::signal_guild_selected() { + return m_signal_guild_selected; +} + +GuildList::type_signal_dms_selected GuildList::signal_dms_selected() { + return m_signal_dms_selected; +} diff --git a/src/components/channellist/classic/guildlist.hpp b/src/components/channellist/classic/guildlist.hpp new file mode 100644 index 0000000..d76e80d --- /dev/null +++ b/src/components/channellist/classic/guildlist.hpp @@ -0,0 +1,31 @@ +#pragma once +#include <gtkmm/listbox.h> +#include "discord/snowflake.hpp" +#include "discord/usersettings.hpp" + +class GuildListGuildItem; + +class GuildList : public Gtk::ListBox { +public: + GuildList(); + + void UpdateListing(); + +private: + void AddGuild(Snowflake id); + void AddFolder(const UserSettingsGuildFoldersEntry &folder); + void Clear(); + + GuildListGuildItem *CreateGuildWidget(Snowflake id); + +public: + using type_signal_guild_selected = sigc::signal<void, Snowflake>; + using type_signal_dms_selected = sigc::signal<void>; + + type_signal_guild_selected signal_guild_selected(); + type_signal_dms_selected signal_dms_selected(); + +private: + type_signal_guild_selected m_signal_guild_selected; + type_signal_dms_selected m_signal_dms_selected; +}; diff --git a/src/components/channellist/classic/guildlistfolderitem.cpp b/src/components/channellist/classic/guildlistfolderitem.cpp new file mode 100644 index 0000000..e79a5f7 --- /dev/null +++ b/src/components/channellist/classic/guildlistfolderitem.cpp @@ -0,0 +1,90 @@ +#include "guildlistfolderitem.hpp" +#include "guildlistguilditem.hpp" + +// doing my best to copy discord here + +const int FolderGridButtonSize = 48; +const int FolderGridImageSize = 24; + +GuildListFolderButton::GuildListFolderButton() { + set_size_request(FolderGridButtonSize, FolderGridButtonSize); +} + +void GuildListFolderButton::SetGuilds(const std::vector<Snowflake> &guild_ids) { + for (int y = 0; y < 2; y++) { + for (int x = 0; x < 2; x++) { + const size_t i = y * 2 + x; + auto &widget = m_images[x][y]; + widget.property_pixbuf() = Abaddon::Get().GetImageManager().GetPlaceholder(FolderGridImageSize); + attach(widget, x, y, 1, 1); + + if (i < guild_ids.size()) { + widget.show(); + + if (const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(guild_ids[i]); guild.has_value()) { + const auto cb = [&widget](const Glib::RefPtr<Gdk::Pixbuf> &pb) { + widget.property_pixbuf() = pb->scale_simple(FolderGridImageSize, FolderGridImageSize, Gdk::INTERP_BILINEAR); + }; + Abaddon::Get().GetImageManager().LoadFromURL(guild->GetIconURL("png", "32"), sigc::track_obj(cb, *this)); + } + } + } + } +} + +GuildListFolderItem::GuildListFolderItem(const UserSettingsGuildFoldersEntry &folder) { + get_style_context()->add_class("classic-guild-folder"); + + if (folder.Name.has_value()) { + set_tooltip_text(*folder.Name); + } + + m_revealer.add(m_box); + m_revealer.set_reveal_child(false); + + m_image.property_pixbuf() = Abaddon::Get().GetImageManager().GetPlaceholder(48); + + 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()); + if (!Abaddon::Get().GetSettings().FolderIconOnly) { + if (m_revealer.get_reveal_child()) { + m_stack.set_visible_child("icon", Gtk::STACK_TRANSITION_TYPE_SLIDE_DOWN); + } else { + m_stack.set_visible_child("grid", Gtk::STACK_TRANSITION_TYPE_SLIDE_UP); + } + } + } + + return false; + }); + + m_grid.SetGuilds(folder.GuildIDs); + m_grid.show(); + + m_icon.property_icon_name() = "folder-symbolic"; + m_icon.property_icon_size() = Gtk::ICON_SIZE_DND; + if (folder.Color.has_value()) { + m_icon.override_color(IntToRGBA(*folder.Color)); + } + m_icon.show(); + + m_stack.add(m_grid, "grid"); + m_stack.add(m_icon, "icon"); + m_stack.set_visible_child(Abaddon::Get().GetSettings().FolderIconOnly ? "icon" : "grid"); + m_stack.show(); + + m_ev.add(m_stack); + add(m_ev); + add(m_revealer); + + m_ev.show(); + m_revealer.show(); + m_box.show(); + m_image.show(); + show(); +} + +void GuildListFolderItem::AddGuildWidget(GuildListGuildItem *widget) { + m_box.add(*widget); +} diff --git a/src/components/channellist/classic/guildlistfolderitem.hpp b/src/components/channellist/classic/guildlistfolderitem.hpp new file mode 100644 index 0000000..6a9fb50 --- /dev/null +++ b/src/components/channellist/classic/guildlistfolderitem.hpp @@ -0,0 +1,36 @@ +#pragma once +#include <gtkmm/box.h> +#include <gtkmm/eventbox.h> +#include <gtkmm/image.h> +#include <gtkmm/revealer.h> + +#include "guildlistguilditem.hpp" +#include "discord/usersettings.hpp" + +class GuildListGuildItem; + +class GuildListFolderButton : public Gtk::Grid { +public: + GuildListFolderButton(); + void SetGuilds(const std::vector<Snowflake> &guild_ids); + +private: + Gtk::Image m_images[2][2]; +}; + +class GuildListFolderItem : public Gtk::VBox { +public: + GuildListFolderItem(const UserSettingsGuildFoldersEntry &folder); + + void AddGuildWidget(GuildListGuildItem *widget); + +private: + Gtk::Stack m_stack; + GuildListFolderButton m_grid; + Gtk::Image m_icon; + + 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..5cec281 --- /dev/null +++ b/src/components/channellist/classic/guildlistguilditem.cpp @@ -0,0 +1,22 @@ +#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(); + + 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<Gdk::Pixbuf> &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 <gtkmm/box.h> +#include <gtkmm/image.h> +#include "discord/guild.hpp" + +class GuildListGuildItem : public Gtk::EventBox { +public: + GuildListGuildItem(const GuildData &guild); + + Snowflake ID; + +private: + void UpdateIcon(); + void OnIconFetched(const Glib::RefPtr<Gdk::Pixbuf> &pb); + + Gtk::Image m_image; +}; diff --git a/src/settings.cpp b/src/settings.cpp index 85464a5..45bb449 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -96,6 +96,8 @@ void SettingsManager::DefineSettings() { AddSetting("gui", "hide_to_try", false, &Settings::HideToTray); AddSetting("gui", "show_deleted_indicator", true, &Settings::ShowDeletedIndicator); AddSetting("gui", "font_scale", -1.0, &Settings::FontScale); + AddSetting("gui", "folder_icon_only", false, &Settings::FolderIconOnly); + AddSetting("gui", "classic_change_guild_on_open", true, &Settings::ClassicChangeGuildOnOpen); AddSetting("gui", "image_embed_clamp_width", 400, &Settings::ImageEmbedClampWidth); AddSetting("gui", "image_embed_clamp_height", 300, &Settings::ImageEmbedClampHeight); diff --git a/src/settings.hpp b/src/settings.hpp index d69d165..65b5b63 100644 --- a/src/settings.hpp +++ b/src/settings.hpp @@ -28,6 +28,8 @@ public: bool HideToTray; bool ShowDeletedIndicator; double FontScale; + bool FolderIconOnly; + bool ClassicChangeGuildOnOpen; int ImageEmbedClampWidth; int ImageEmbedClampHeight; diff --git a/src/windows/mainwindow.cpp b/src/windows/mainwindow.cpp index 8e030ed..ea67d2b 100644 --- a/src/windows/mainwindow.cpp +++ b/src/windows/mainwindow.cpp @@ -33,6 +33,8 @@ MainWindow::MainWindow() }); #endif + // TEMP TEMP TEMP TEMP!!!!!!!!!!!! AHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH + m_channel_list.SetClassic(true); m_channel_list.set_vexpand(true); m_channel_list.set_size_request(-1, -1); m_channel_list.show(); diff --git a/src/windows/mainwindow.hpp b/src/windows/mainwindow.hpp index ce2e636..37c1b87 100644 --- a/src/windows/mainwindow.hpp +++ b/src/windows/mainwindow.hpp @@ -1,5 +1,5 @@ #pragma once -#include "components/channels.hpp" +#include "components/channellist/channellist.hpp" #include "components/chatwindow.hpp" #include "components/memberlist.hpp" #include "components/friendslist.hpp" |